diff --git a/src/app/shared/search/search-results/search-results.component.scss b/src/app/shared/search/search-results/search-results.component.scss
new file mode 100644
index 0000000000..6e369c729b
--- /dev/null
+++ b/src/app/shared/search/search-results/search-results.component.scss
@@ -0,0 +1,17 @@
+:host ::ng-deep {
+ .filter-badge-skeleton {
+ ngx-skeleton-loader .skeleton-loader {
+ background-color: var(--bs-light);
+ box-shadow: none;
+ width: var(--ds-search-skeleton-filter-badge-width);
+ height: var(--ds-search-skeleton-badge-height);
+ margin-bottom: 0;
+ margin-right: calc(var(--bs-spacer) / 4);
+ }
+ }
+
+ .filters-badge-skeleton-container {
+ display: flex;
+ max-height: var(--ds-search-skeleton-badge-height);
+ }
+}
diff --git a/src/app/shared/search/search-results/search-results.component.spec.ts b/src/app/shared/search/search-results/search-results.component.spec.ts
index fedea57e07..33b8432b74 100644
--- a/src/app/shared/search/search-results/search-results.component.spec.ts
+++ b/src/app/shared/search/search-results/search-results.component.spec.ts
@@ -13,16 +13,20 @@ import { ActivatedRoute } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { Community } from '../../../core/shared/community.model';
+import { SearchService } from '../../../core/shared/search/search.service';
+import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
import { ErrorComponent } from '../../error/error.component';
-import { ThemedLoadingComponent } from '../../loading/themed-loading.component';
import { getMockThemeService } from '../../mocks/theme-service.mock';
import { ObjectCollectionComponent } from '../../object-collection/object-collection.component';
import { createFailedRemoteDataObject } from '../../remote-data.utils';
import { ActivatedRouteStub } from '../../testing/active-router.stub';
import { QueryParamsDirectiveStub } from '../../testing/query-params-directive.stub';
+import { SearchConfigurationServiceStub } from '../../testing/search-configuration-service.stub';
+import { SearchServiceStub } from '../../testing/search-service.stub';
import { ThemeService } from '../../theme-support/theme.service';
import { SearchExportCsvComponent } from '../search-export-csv/search-export-csv.component';
import { SearchResultsComponent } from './search-results.component';
+import { SearchResultsSkeletonComponent } from './search-results-skeleton/search-results-skeleton.component';
describe('SearchResultsComponent', () => {
let comp: SearchResultsComponent;
@@ -35,6 +39,11 @@ describe('SearchResultsComponent', () => {
providers: [
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
{ provide: ThemeService, useValue: getMockThemeService() },
+ { provide: SearchService, useValue: new SearchServiceStub() },
+ {
+ provide: SearchConfigurationService,
+ useValue: new SearchConfigurationServiceStub(),
+ },
],
imports: [
TranslateModule.forRoot(),
@@ -48,8 +57,8 @@ describe('SearchResultsComponent', () => {
imports: [
SearchExportCsvComponent,
ObjectCollectionComponent,
- ThemedLoadingComponent,
ErrorComponent,
+ SearchResultsSkeletonComponent,
],
},
add: { imports: [QueryParamsDirectiveStub] },
@@ -96,7 +105,7 @@ describe('SearchResultsComponent', () => {
it('should display link with new search where query is quoted if search return a error 400', () => {
(comp as any).searchResults = createFailedRemoteDataObject('Error', 400);
- (comp as any).searchConfig = { query: 'foobar' };
+ (comp as any).searchConfig = { query: 'foobar', pagination: { pageSize: 10 } };
fixture.detectChanges();
const linkDes = fixture.debugElement.queryAll(By.directive(QueryParamsDirectiveStub));
diff --git a/src/app/shared/search/search-results/search-results.component.ts b/src/app/shared/search/search-results/search-results.component.ts
index 09186e8c6c..f9da77ed75 100644
--- a/src/app/shared/search/search-results/search-results.component.ts
+++ b/src/app/shared/search/search-results/search-results.component.ts
@@ -1,4 +1,7 @@
-import { NgIf } from '@angular/common';
+import {
+ AsyncPipe,
+ NgIf,
+} from '@angular/common';
import {
Component,
EventEmitter,
@@ -7,12 +10,19 @@ import {
} from '@angular/core';
import { RouterLink } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
+import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
+import {
+ BehaviorSubject,
+ Observable,
+} from 'rxjs';
import { SortOptions } from '../../../core/cache/models/sort-options.model';
import { PaginatedList } from '../../../core/data/paginated-list.model';
import { RemoteData } from '../../../core/data/remote-data';
import { Context } from '../../../core/shared/context.model';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
+import { SearchService } from '../../../core/shared/search/search.service';
+import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
import { ViewMode } from '../../../core/shared/view-mode.model';
import {
fadeIn,
@@ -23,13 +33,15 @@ import {
isNotEmpty,
} from '../../empty.util';
import { ErrorComponent } from '../../error/error.component';
-import { ThemedLoadingComponent } from '../../loading/themed-loading.component';
import { CollectionElementLinkType } from '../../object-collection/collection-element-link.type';
import { ObjectCollectionComponent } from '../../object-collection/object-collection.component';
import { ListableObject } from '../../object-collection/shared/listable-object.model';
+import { AppliedFilter } from '../models/applied-filter.model';
import { PaginatedSearchOptions } from '../models/paginated-search-options.model';
+import { SearchFilter } from '../models/search-filter.model';
import { SearchResult } from '../models/search-result.model';
import { SearchExportCsvComponent } from '../search-export-csv/search-export-csv.component';
+import { SearchResultsSkeletonComponent } from './search-results-skeleton/search-results-skeleton.component';
export interface SelectionConfig {
repeatable: boolean;
@@ -39,12 +51,13 @@ export interface SelectionConfig {
@Component({
selector: 'ds-base-search-results',
templateUrl: './search-results.component.html',
+ styleUrls: ['./search-results.component.scss'],
animations: [
fadeIn,
fadeInOut,
],
standalone: true,
- imports: [NgIf, SearchExportCsvComponent, ObjectCollectionComponent, ThemedLoadingComponent, ErrorComponent, RouterLink, TranslateModule],
+ imports: [NgIf, SearchExportCsvComponent, ObjectCollectionComponent, ErrorComponent, RouterLink, TranslateModule, SearchResultsSkeletonComponent, AsyncPipe, NgxSkeletonLoaderModule],
})
/**
@@ -52,6 +65,15 @@ export interface SelectionConfig {
*/
export class SearchResultsComponent {
hasNoValue = hasNoValue;
+ /**
+ * Currently active filters in url
+ */
+ activeFilters$: Observable
;
+
+ /**
+ * Filter applied to show labels, once populated the activeFilters$ will be loaded
+ */
+ appliedFilters$: BehaviorSubject;
/**
* The link type of the listed search results
@@ -125,10 +147,18 @@ export class SearchResultsComponent {
@Output() selectObject: EventEmitter = new EventEmitter();
+ constructor(
+ protected searchConfigService: SearchConfigurationService,
+ protected searchService: SearchService,
+ ) {
+ this.activeFilters$ = this.searchConfigService.getCurrentFilters();
+ this.appliedFilters$ = this.searchService.appliedFilters$;
+ }
+
/**
* Check if search results are loading
*/
- isLoading() {
+ isLoading(): boolean {
return !this.showError() && (hasNoValue(this.searchResults) || hasNoValue(this.searchResults.payload) || this.searchResults.isLoading);
}
diff --git a/src/app/shared/search/search.component.spec.ts b/src/app/shared/search/search.component.spec.ts
index 31c8a6e9b8..8b533b4105 100644
--- a/src/app/shared/search/search.component.spec.ts
+++ b/src/app/shared/search/search.component.spec.ts
@@ -1,6 +1,7 @@
import {
ChangeDetectionStrategy,
NO_ERRORS_SCHEMA,
+ PLATFORM_ID,
} from '@angular/core';
import {
ComponentFixture,
@@ -246,6 +247,7 @@ export function configureSearchComponentTestingModule(compType, additionalDeclar
},
{ provide: APP_DATA_SERVICES_MAP, useValue: {} },
{ provide: APP_CONFIG, useValue: environment },
+ { provide: PLATFORM_ID, useValue: 'browser' },
],
schemas: [NO_ERRORS_SCHEMA],
}).overrideComponent(compType, {
@@ -415,5 +417,34 @@ describe('SearchComponent', () => {
expect(result).toBeNull();
});
});
+
+ describe('when rendered in SSR', () => {
+ beforeEach(() => {
+ comp.platformId = 'server';
+ });
+
+ it('should not call search method on init', (done) => {
+ comp.ngOnInit();
+ //Check that the first method from which the search depend upon is not being called
+ expect(searchConfigurationServiceStub.getCurrentConfiguration).not.toHaveBeenCalled();
+ comp.initialized$.subscribe((res) => {
+ expect(res).toBeTruthy();
+ done();
+ });
+ });
+ });
+
+ describe('when rendered in CSR', () => {
+ beforeEach(() => {
+ comp.platformId = 'browser';
+ });
+
+ it('should call search method on init', fakeAsync(() => {
+ comp.ngOnInit();
+ tick(100);
+ //Check that the last method from which the search depend upon is being called
+ expect(searchServiceStub.search).toHaveBeenCalled();
+ }));
+ });
});
});
diff --git a/src/app/shared/search/search.component.ts b/src/app/shared/search/search.component.ts
index 337cb67171..45c7eb6f7e 100644
--- a/src/app/shared/search/search.component.ts
+++ b/src/app/shared/search/search.component.ts
@@ -1,5 +1,6 @@
import {
AsyncPipe,
+ isPlatformServer,
NgIf,
NgTemplateOutlet,
} from '@angular/common';
@@ -12,6 +13,7 @@ import {
OnDestroy,
OnInit,
Output,
+ PLATFORM_ID,
} from '@angular/core';
import {
NavigationStart,
@@ -37,6 +39,7 @@ import {
APP_CONFIG,
AppConfig,
} from '../../../config/app-config.interface';
+import { environment } from '../../../environments/environment';
import { COLLECTION_MODULE_PATH } from '../../collection-page/collection-page-routing-paths';
import { COMMUNITY_MODULE_PATH } from '../../community-page/community-page-routing-paths';
import { SortOptions } from '../../core/cache/models/sort-options.model';
@@ -236,6 +239,11 @@ export class SearchComponent implements OnDestroy, OnInit {
*/
@Input() hideScopeInUrl: boolean;
+ /**
+ * Defines whether to fetch search results during SSR execution
+ */
+ @Input() renderOnServerSide: boolean;
+
/**
* The current configuration used during the search
*/
@@ -251,6 +259,7 @@ export class SearchComponent implements OnDestroy, OnInit {
*/
currentScope$: Observable;
+
/**
* The current sort options used
*/
@@ -345,6 +354,7 @@ export class SearchComponent implements OnDestroy, OnInit {
protected routeService: RouteService,
protected router: Router,
@Inject(APP_CONFIG) protected appConfig: AppConfig,
+ @Inject(PLATFORM_ID) public platformId: any,
) {
this.isXsOrSm$ = this.windowService.isXsOrSm();
}
@@ -357,6 +367,14 @@ export class SearchComponent implements OnDestroy, OnInit {
* If something changes, update the list of scopes for the dropdown
*/
ngOnInit(): void {
+ if (!this.renderOnServerSide && !environment.ssr.enableSearchComponent && isPlatformServer(this.platformId)) {
+ this.subs.push(this.getSearchOptions().pipe(distinctUntilChanged()).subscribe((options) => {
+ this.searchOptions$.next(options);
+ }));
+ this.initialized$.next(true);
+ return;
+ }
+
if (this.useUniquePageId) {
// Create an unique pagination id related to the instance of the SearchComponent
this.paginationId = uniqueId(this.paginationId);
diff --git a/src/app/shared/testing/search-configuration-service.stub.ts b/src/app/shared/testing/search-configuration-service.stub.ts
index c567808e87..426dc72930 100644
--- a/src/app/shared/testing/search-configuration-service.stub.ts
+++ b/src/app/shared/testing/search-configuration-service.stub.ts
@@ -26,6 +26,10 @@ export class SearchConfigurationServiceStub {
return observableOf([]);
}
+ getCurrentFilters() {
+ return observableOf([]);
+ }
+
getCurrentScope(a) {
return observableOf('test-id');
}
diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts
index 3455ef5f92..3c5e0ef0da 100644
--- a/src/config/default-app-config.ts
+++ b/src/config/default-app-config.ts
@@ -516,6 +516,7 @@ export class DefaultAppConfig implements AppConfig {
enabled: false,
filter: ['title', 'author', 'subject', 'entityType'],
},
+ filterPlaceholdersCount: 5,
};
notifyMetrics: AdminNotifyMetricsRow[] = [
diff --git a/src/config/search-page-config.interface.ts b/src/config/search-page-config.interface.ts
index 699e3fd61f..55733c98e4 100644
--- a/src/config/search-page-config.interface.ts
+++ b/src/config/search-page-config.interface.ts
@@ -8,5 +8,11 @@ export interface SearchConfig extends Config {
* Used by {@link UploadBitstreamComponent}.
*/
advancedFilters: AdvancedSearchConfig;
-
+ /**
+ * Number used to render n UI elements called loading skeletons that act as placeholders.
+ * These elements indicate that some content will be loaded in their stead.
+ * Since we don't know how many filters will be loaded before we receive a response from the server we use this parameter for the skeletons count.
+ * For instance if we set 5 then 5 loading skeletons will be visualized before the actual filters are retrieved.
+ */
+ filterPlaceholdersCount?: number;
}
diff --git a/src/config/ssr-config.interface.ts b/src/config/ssr-config.interface.ts
index 1a01e4d125..4def73962a 100644
--- a/src/config/ssr-config.interface.ts
+++ b/src/config/ssr-config.interface.ts
@@ -25,4 +25,14 @@ export interface SSRConfig extends Config {
* Paths to enable SSR for. Defaults to the home page and paths in the sitemap.
*/
paths: Array;
+
+ /**
+ * Whether to enable rendering of search component on SSR
+ */
+ enableSearchComponent: boolean;
+
+ /**
+ * Whether to enable rendering of browse component on SSR
+ */
+ enableBrowseComponent: boolean;
}
diff --git a/src/environments/environment.production.ts b/src/environments/environment.production.ts
index 35b0d39408..8159fff2ea 100644
--- a/src/environments/environment.production.ts
+++ b/src/environments/environment.production.ts
@@ -9,5 +9,7 @@ export const environment: Partial = {
enablePerformanceProfiler: false,
inlineCriticalCss: false,
paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/' ],
+ enableSearchComponent: false,
+ enableBrowseComponent: false,
},
};
diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts
index 84229aa659..17d4e8ed10 100644
--- a/src/environments/environment.test.ts
+++ b/src/environments/environment.test.ts
@@ -13,6 +13,8 @@ export const environment: BuildConfig = {
enablePerformanceProfiler: false,
inlineCriticalCss: false,
paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/' ],
+ enableSearchComponent: false,
+ enableBrowseComponent: false,
},
// Angular express server settings.
diff --git a/src/environments/environment.ts b/src/environments/environment.ts
index a02ef4a2a9..4ac586d899 100644
--- a/src/environments/environment.ts
+++ b/src/environments/environment.ts
@@ -14,6 +14,8 @@ export const environment: Partial = {
enablePerformanceProfiler: false,
inlineCriticalCss: false,
paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/' ],
+ enableSearchComponent: false,
+ enableBrowseComponent: false,
},
};
diff --git a/src/styles/_custom_variables.scss b/src/styles/_custom_variables.scss
index 6bddfd58e9..6bd2bef378 100644
--- a/src/styles/_custom_variables.scss
+++ b/src/styles/_custom_variables.scss
@@ -150,4 +150,16 @@
--green1: #1c710a; // This variable represents the success color for the Orejime cookie banner
--button-text-color-cookie: #fff; // This variable represents the text color for buttons in the Orejime cookie banner
+
+ --ds-search-skeleton-text-height: 20px;
+ --ds-search-skeleton-badge-height: 18px;
+ --ds-search-skeleton-thumbnail-margin: 1em;
+ --ds-search-skeleton-text-line-count: 2;
+ --ds-search-skeleton-badge-width: 75px;
+ --ds-search-skeleton-filter-badge-width: 200px;
+ --ds-search-skeleton-info-width: 200px;
+ --ds-search-skeleton-card-height: 435px;
+
+ --ds-filters-skeleton-height: 40px;
+ --ds-filters-skeleton-spacing: 12px;
}
diff --git a/src/themes/custom/app/shared/search/search-filters/search-filters.component.ts b/src/themes/custom/app/shared/search/search-filters/search-filters.component.ts
index 5b10c84145..3e86e16c01 100644
--- a/src/themes/custom/app/shared/search/search-filters/search-filters.component.ts
+++ b/src/themes/custom/app/shared/search/search-filters/search-filters.component.ts
@@ -5,18 +5,14 @@
*
* https://www.atmire.com/software-license/
*/
-import {
- AsyncPipe,
- NgFor,
- NgIf,
-} from '@angular/common';
+import { AsyncPipe } from '@angular/common';
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
+import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { SearchConfigurationService } from '../../../../../../app/core/shared/search/search-configuration.service';
import { SEARCH_CONFIG_SERVICE } from '../../../../../../app/my-dspace-page/my-dspace-configuration.service';
-import { AdvancedSearchComponent } from '../../../../../../app/shared/search/advanced-search/advanced-search.component';
import { SearchFilterComponent } from '../../../../../../app/shared/search/search-filters/search-filter/search-filter.component';
import { SearchFiltersComponent as BaseComponent } from '../../../../../../app/shared/search/search-filters/search-filters.component';
@@ -34,7 +30,7 @@ import { SearchFiltersComponent as BaseComponent } from '../../../../../../app/s
},
],
standalone: true,
- imports: [NgIf, NgFor, SearchFilterComponent, RouterLink, AsyncPipe, TranslateModule, AdvancedSearchComponent],
+ imports: [SearchFilterComponent, RouterLink, AsyncPipe, TranslateModule, NgxSkeletonLoaderModule],
})
export class SearchFiltersComponent extends BaseComponent {
diff --git a/src/themes/custom/app/shared/search/search-results/search-results.component.ts b/src/themes/custom/app/shared/search/search-results/search-results.component.ts
index b72297f32f..e2c4427744 100644
--- a/src/themes/custom/app/shared/search/search-results/search-results.component.ts
+++ b/src/themes/custom/app/shared/search/search-results/search-results.component.ts
@@ -1,29 +1,33 @@
-import { NgIf } from '@angular/common';
+import {
+ AsyncPipe,
+ NgIf,
+} from '@angular/common';
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
+import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import {
fadeIn,
fadeInOut,
} from '../../../../../../app/shared/animations/fade';
import { ErrorComponent } from '../../../../../../app/shared/error/error.component';
-import { ThemedLoadingComponent } from '../../../../../../app/shared/loading/themed-loading.component';
import { ObjectCollectionComponent } from '../../../../../../app/shared/object-collection/object-collection.component';
import { SearchExportCsvComponent } from '../../../../../../app/shared/search/search-export-csv/search-export-csv.component';
import { SearchResultsComponent as BaseComponent } from '../../../../../../app/shared/search/search-results/search-results.component';
+import { SearchResultsSkeletonComponent } from '../../../../../../app/shared/search/search-results/search-results-skeleton/search-results-skeleton.component';
@Component({
selector: 'ds-themed-search-results',
// templateUrl: './search-results.component.html',
templateUrl: '../../../../../../app/shared/search/search-results/search-results.component.html',
- // styleUrls: ['./search-results.component.scss'],
+ styleUrls: ['../../../../../../app/shared/search/search-results/search-results.component.scss'],
animations: [
fadeIn,
fadeInOut,
],
standalone: true,
- imports: [NgIf, SearchExportCsvComponent, ObjectCollectionComponent, ThemedLoadingComponent, ErrorComponent, RouterLink, TranslateModule],
+ imports: [NgIf, SearchExportCsvComponent, ObjectCollectionComponent, ErrorComponent, RouterLink, TranslateModule, SearchResultsSkeletonComponent, SearchResultsSkeletonComponent, AsyncPipe, NgxSkeletonLoaderModule],
})
export class SearchResultsComponent extends BaseComponent {
diff --git a/src/themes/custom/lazy-theme.module.ts b/src/themes/custom/lazy-theme.module.ts
index 4ea749794e..14e42d09e4 100644
--- a/src/themes/custom/lazy-theme.module.ts
+++ b/src/themes/custom/lazy-theme.module.ts
@@ -12,6 +12,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
import { RootModule } from '../../app/root.module';
+import { SearchResultsSkeletonComponent } from '../../app/shared/search/search-results/search-results-skeleton/search-results-skeleton.component';
import { MetadataImportPageComponent } from './app/admin/admin-import-metadata-page/metadata-import-page.component';
import { AdminSearchPageComponent } from './app/admin/admin-search-page/admin-search-page.component';
import { AdminSidebarComponent } from './app/admin/admin-sidebar/admin-sidebar.component';
@@ -105,6 +106,7 @@ import { WorkflowItemDeleteComponent } from './app/workflowitems-edit-page/workf
import { WorkflowItemSendBackComponent } from './app/workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component';
import { WorkspaceItemsDeletePageComponent } from './app/workspaceitems-edit-page/workspaceitems-delete-page/workspaceitems-delete-page.component';
+
const DECLARATIONS = [
FileSectionComponent,
HomePageComponent,
@@ -198,6 +200,7 @@ const DECLARATIONS = [
ComcolPageContentComponent,
AdminSearchPageComponent,
AdminWorkflowPageComponent,
+ SearchResultsSkeletonComponent,
];
@NgModule({
diff --git a/src/themes/dspace/styles/_theme_css_variable_overrides.scss b/src/themes/dspace/styles/_theme_css_variable_overrides.scss
index 63a5535d95..863db57833 100644
--- a/src/themes/dspace/styles/_theme_css_variable_overrides.scss
+++ b/src/themes/dspace/styles/_theme_css_variable_overrides.scss
@@ -18,6 +18,7 @@
/* set the next two properties as `--ds-header-navbar-border-bottom-*`
in order to keep the bottom border of the header when navbar is expanded */
+
--ds-expandable-navbar-border-top-color: #{$white};
--ds-expandable-navbar-border-top-height: 0;
--ds-expandable-navbar-padding-top: 0;
From 33a091d6301a476e94d9eb9b0ca0d2d704fe44cb Mon Sep 17 00:00:00 2001
From: FrancescoMolinaro <152612515+FrancescoMolinaro@users.noreply.github.com>
Date: Thu, 23 Jan 2025 19:26:36 +0100
Subject: [PATCH 19/19] Prevent request with page size of 9999 (#3694)
* [DURACOM-304] Refactored item-bitstreams.component by removing page size of 9999
* [DURACOM-304] Refactored edit-bitstream-page.component by removing page size of 9999
* [DURACOM-304] Refactored scripts-select.component by using infinite scroll instead of page size 9999
* [DURACOM-304] Refactored dynamic-list.component.ts by removing page size of 9999
* [DURACOM-304] Refactored relationship-type-data.service.ts by removing page size of 9999
* [DURACOM-304] removed unneeded selectAll method (dynamic-lookup-relation-search-tab.component)
* [DURACOM-304] Refactored submission-section-cc-licenses.component.ts by removing page size of 9999
* [DURACOM-304] lint fix
* [DURACOM-304] test fix
* [DURACOM-304] fix accessibility issue on scripts-select
* [DURACOM-304] Refactor of bundle-data.service.ts by removing page size of 9999
* [DURACOM-304] other fix related to accessibility
* [DURACOM-304] lint fix
* [DURACOM-304] resolve conflicts
* [DURACOM-304] fix lint
* [DURACOM-304] add support for findAll method in dynamic-scrollable-dropdown.component.ts
* [DURACOM-304] refactor to use lazy data provider
* [DURACOM-304] improve loading logic for cc-licenses section and dynamic-list
* [DURACOM-304] refactor, fix dynamic-list.component loading
* [DURACOM-304] remove br
---------
Co-authored-by: Alisa Ismailati
---
.../edit-bitstream-page.component.html | 6 +-
.../edit-bitstream-page.component.spec.ts | 17 ++-
.../edit-bitstream-page.component.ts | 124 ++++++++---------
src/app/core/data/bitstream-data.service.ts | 5 +-
src/app/core/data/bundle-data.service.ts | 8 +-
src/app/core/metadata/head-tag.service.ts | 2 +
.../item-bitstreams.component.html | 3 +
.../item-bitstreams.component.ts | 129 ++++++++++++++++--
.../scripts-select.component.html | 59 +++++---
.../scripts-select.component.scss | 23 ++++
.../scripts-select.component.spec.ts | 4 +-
.../scripts-select.component.ts | 113 +++++++++------
.../models/list/dynamic-list.component.html | 8 +-
.../models/list/dynamic-list.component.ts | 111 +++++++++++----
...dynamic-scrollable-dropdown.component.html | 4 +-
...amic-scrollable-dropdown.component.spec.ts | 4 +
.../dynamic-scrollable-dropdown.component.ts | 82 +++++++++--
.../dynamic-scrollable-dropdown.model.ts | 18 ++-
...okup-relation-search-tab.component.spec.ts | 12 --
...ic-lookup-relation-search-tab.component.ts | 38 +-----
...mission-section-cc-licenses.component.html | 69 +++++-----
...mission-section-cc-licenses.component.scss | 10 ++
...sion-section-cc-licenses.component.spec.ts | 8 +-
...ubmission-section-cc-licenses.component.ts | 71 ++++++++--
src/assets/i18n/en.json5 | 4 +
25 files changed, 638 insertions(+), 294 deletions(-)
diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.html b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.html
index f7d2c60832..259ab599cb 100644
--- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.html
+++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.html
@@ -1,6 +1,6 @@
-
-
diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts
index 9243a36491..7da9e040ce 100644
--- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts
+++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts
@@ -261,7 +261,7 @@ describe('EditBitstreamPageComponent', () => {
});
it('should select the correct format', () => {
- expect(rawForm.formatContainer.selectedFormat).toEqual(selectedFormat.id);
+ expect(rawForm.formatContainer.selectedFormat).toEqual(selectedFormat.shortDescription);
});
it('should put the \"New Format\" input on invisible', () => {
@@ -292,7 +292,13 @@ describe('EditBitstreamPageComponent', () => {
describe('when an unknown format is selected', () => {
beforeEach(() => {
- comp.updateNewFormatLayout(allFormats[0].id);
+ comp.onChange({
+ model: {
+ id: 'selectedFormat',
+ value: allFormats[0],
+ },
+ });
+ comp.updateNewFormatLayout();
});
it('should remove the invisible class from the \"New Format\" input', () => {
@@ -394,9 +400,10 @@ describe('EditBitstreamPageComponent', () => {
describe('when selected format has changed', () => {
beforeEach(() => {
- comp.formGroup.patchValue({
- formatContainer: {
- selectedFormat: allFormats[2].id,
+ comp.onChange({
+ model: {
+ id: 'selectedFormat',
+ value: allFormats[2],
},
});
fixture.detectChanges();
diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts
index 36b0816ade..9f1a84c19d 100644
--- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts
+++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts
@@ -21,7 +21,6 @@ import {
DynamicFormLayout,
DynamicFormService,
DynamicInputModel,
- DynamicSelectModel,
} from '@ng-dynamic-forms/core';
import {
TranslateModule,
@@ -39,23 +38,24 @@ import {
filter,
map,
switchMap,
+ take,
tap,
} from 'rxjs/operators';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
+import { FindAllDataImpl } from '../../core/data/base/find-all-data';
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
import { BitstreamFormatDataService } from '../../core/data/bitstream-format-data.service';
-import { PaginatedList } from '../../core/data/paginated-list.model';
import { PrimaryBitstreamService } from '../../core/data/primary-bitstream.service';
import { RemoteData } from '../../core/data/remote-data';
import { Bitstream } from '../../core/shared/bitstream.model';
import { BitstreamFormat } from '../../core/shared/bitstream-format.model';
+import { BITSTREAM_FORMAT } from '../../core/shared/bitstream-format.resource-type';
import { BitstreamFormatSupportLevel } from '../../core/shared/bitstream-format-support-level';
import { Bundle } from '../../core/shared/bundle.model';
import { Item } from '../../core/shared/item.model';
import { Metadata } from '../../core/shared/metadata.utils';
import {
- getAllSucceededRemoteDataPayload,
getFirstCompletedRemoteData,
getFirstSucceededRemoteData,
getFirstSucceededRemoteDataPayload,
@@ -72,6 +72,7 @@ import { ErrorComponent } from '../../shared/error/error.component';
import { DynamicCustomSwitchModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model';
import { DsDynamicInputModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model';
import { DsDynamicTextAreaModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model';
+import { DynamicScrollableDropdownModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
import { FormComponent } from '../../shared/form/form.component';
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
import { NotificationsService } from '../../shared/notifications/notifications.service';
@@ -109,12 +110,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
*/
bitstreamRD$: Observable
>;
- /**
- * The formats their remote data observable
- * Tracks changes and updates the view
- */
- bitstreamFormatsRD$: Observable>>;
-
/**
* The UUID of the primary bitstream for this bundle
*/
@@ -130,11 +125,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
*/
originalFormat: BitstreamFormat;
- /**
- * A list of all available bitstream formats
- */
- formats: BitstreamFormat[];
-
/**
* @type {string} Key prefix used to generate form messages
*/
@@ -178,7 +168,10 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
/**
* Options for fetching all bitstream formats
*/
- findAllOptions = { elementsPerPage: 9999 };
+ findAllOptions = {
+ elementsPerPage: 20,
+ currentPage: 1,
+ };
/**
* The Dynamic Input Model for the file's name
@@ -218,9 +211,22 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
/**
* The Dynamic Input Model for the selected format
*/
- selectedFormatModel = new DynamicSelectModel({
+ selectedFormatModel = new DynamicScrollableDropdownModel({
id: 'selectedFormat',
name: 'selectedFormat',
+ displayKey: 'shortDescription',
+ repeatable: false,
+ metadataFields: [],
+ submissionId: '',
+ hasSelectableMetadata: false,
+ resourceType: BITSTREAM_FORMAT,
+ formatFunction: (format: BitstreamFormat | string) => {
+ if (format instanceof BitstreamFormat) {
+ return hasValue(format) && format.supportLevel === BitstreamFormatSupportLevel.Unknown ? this.translate.instant(this.KEY_PREFIX + 'selectedFormat.unknown') : format.shortDescription;
+ } else {
+ return format;
+ }
+ },
});
/**
@@ -438,6 +444,11 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
* @private
*/
private bundle: Bundle;
+ /**
+ * The currently selected format
+ * @private
+ */
+ private selectedFormat: BitstreamFormat;
constructor(private route: ActivatedRoute,
private router: Router,
@@ -463,18 +474,12 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
this.itemId = this.route.snapshot.queryParams.itemId;
this.entityType = this.route.snapshot.queryParams.entityType;
this.bitstreamRD$ = this.route.data.pipe(map((data: any) => data.bitstream));
- this.bitstreamFormatsRD$ = this.bitstreamFormatService.findAll(this.findAllOptions);
const bitstream$ = this.bitstreamRD$.pipe(
getFirstSucceededRemoteData(),
getRemoteDataPayload(),
);
- const allFormats$ = this.bitstreamFormatsRD$.pipe(
- getFirstSucceededRemoteData(),
- getRemoteDataPayload(),
- );
-
const bundle$ = bitstream$.pipe(
switchMap((bitstream: Bitstream) => bitstream.bundle),
getFirstSucceededRemoteDataPayload(),
@@ -490,24 +495,31 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
switchMap((bundle: Bundle) => bundle.item),
getFirstSucceededRemoteDataPayload(),
);
+ const format$ = bitstream$.pipe(
+ switchMap(bitstream => bitstream.format),
+ getFirstSucceededRemoteDataPayload(),
+ );
+
this.subs.push(
observableCombineLatest(
bitstream$,
- allFormats$,
bundle$,
primaryBitstream$,
item$,
- ).pipe()
- .subscribe(([bitstream, allFormats, bundle, primaryBitstream, item]) => {
- this.bitstream = bitstream as Bitstream;
- this.formats = allFormats.page;
- this.bundle = bundle;
- // hasValue(primaryBitstream) because if there's no primaryBitstream on the bundle it will
- // be a success response, but empty
- this.primaryBitstreamUUID = hasValue(primaryBitstream) ? primaryBitstream.uuid : null;
- this.itemId = item.uuid;
- this.setIiifStatus(this.bitstream);
- }),
+ format$,
+ ).subscribe(([bitstream, bundle, primaryBitstream, item, format]) => {
+ this.bitstream = bitstream as Bitstream;
+ this.bundle = bundle;
+ this.selectedFormat = format;
+ // hasValue(primaryBitstream) because if there's no primaryBitstream on the bundle it will
+ // be a success response, but empty
+ this.primaryBitstreamUUID = hasValue(primaryBitstream) ? primaryBitstream.uuid : null;
+ this.itemId = item.uuid;
+ this.setIiifStatus(this.bitstream);
+ }),
+ format$.pipe(take(1)).subscribe(
+ (format) => this.originalFormat = format,
+ ),
);
this.subs.push(
@@ -523,7 +535,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
*/
setForm() {
this.formGroup = this.formService.createFormGroup(this.formModel);
- this.updateFormatModel();
this.updateForm(this.bitstream);
this.updateFieldTranslations();
}
@@ -542,6 +553,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
description: bitstream.firstMetadataValue('dc.description'),
},
formatContainer: {
+ selectedFormat: this.selectedFormat.shortDescription,
newFormat: hasValue(bitstream.firstMetadata('dc.format')) ? bitstream.firstMetadata('dc.format').value : undefined,
},
});
@@ -561,36 +573,16 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
},
});
}
- this.bitstream.format.pipe(
- getAllSucceededRemoteDataPayload(),
- ).subscribe((format: BitstreamFormat) => {
- this.originalFormat = format;
- this.formGroup.patchValue({
- formatContainer: {
- selectedFormat: format.id,
- },
- });
- this.updateNewFormatLayout(format.id);
- });
+ this.updateNewFormatLayout();
}
- /**
- * Create the list of unknown format IDs an add options to the selectedFormatModel
- */
- updateFormatModel() {
- this.selectedFormatModel.options = this.formats.map((format: BitstreamFormat) =>
- Object.assign({
- value: format.id,
- label: this.isUnknownFormat(format.id) ? this.translate.instant(this.KEY_PREFIX + 'selectedFormat.unknown') : format.shortDescription,
- }));
- }
/**
* Update the layout of the "Other Format" input depending on the selected format
* @param selectedId
*/
- updateNewFormatLayout(selectedId: string) {
- if (this.isUnknownFormat(selectedId)) {
+ updateNewFormatLayout() {
+ if (this.isUnknownFormat()) {
this.formLayout.newFormat.grid.host = this.newFormatBaseLayout;
} else {
this.formLayout.newFormat.grid.host = this.newFormatBaseLayout + ' invisible';
@@ -601,9 +593,8 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
* Is the provided format (id) part of the list of unknown formats?
* @param id
*/
- isUnknownFormat(id: string): boolean {
- const format = this.formats.find((f: BitstreamFormat) => f.id === id);
- return hasValue(format) && format.supportLevel === BitstreamFormatSupportLevel.Unknown;
+ isUnknownFormat(): boolean {
+ return hasValue(this.selectedFormat) && this.selectedFormat.supportLevel === BitstreamFormatSupportLevel.Unknown;
}
/**
@@ -635,7 +626,8 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
onChange(event) {
const model = event.model;
if (model.id === this.selectedFormatModel.id) {
- this.updateNewFormatLayout(model.value);
+ this.selectedFormat = model.value;
+ this.updateNewFormatLayout();
}
}
@@ -645,8 +637,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
onSubmit() {
const updatedValues = this.formGroup.getRawValue();
const updatedBitstream = this.formToBitstream(updatedValues);
- const selectedFormat = this.formats.find((f: BitstreamFormat) => f.id === updatedValues.formatContainer.selectedFormat);
- const isNewFormat = selectedFormat.id !== this.originalFormat.id;
+ const isNewFormat = this.selectedFormat.id !== this.originalFormat.id;
const isPrimary = updatedValues.fileNamePrimaryContainer.primaryBitstream;
const wasPrimary = this.primaryBitstreamUUID === this.bitstream.uuid;
@@ -698,7 +689,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
bundle$ = observableOf(this.bundle);
}
if (isNewFormat) {
- bitstream$ = this.bitstreamService.updateFormat(this.bitstream, selectedFormat).pipe(
+ bitstream$ = this.bitstreamService.updateFormat(this.bitstream, this.selectedFormat).pipe(
getFirstCompletedRemoteData(),
map((formatResponse: RemoteData) => {
if (hasValue(formatResponse) && formatResponse.hasFailed) {
@@ -856,4 +847,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
.forEach((subscription) => subscription.unsubscribe());
}
+ findAllFormatsServiceFactory() {
+ return () => this.bitstreamFormatService as any as FindAllDataImpl;
+ }
}
diff --git a/src/app/core/data/bitstream-data.service.ts b/src/app/core/data/bitstream-data.service.ts
index b8776d2530..9455a456fa 100644
--- a/src/app/core/data/bitstream-data.service.ts
+++ b/src/app/core/data/bitstream-data.service.ts
@@ -241,11 +241,12 @@ export class BitstreamDataService extends IdentifiableDataService imp
* no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
+ * @param options the {@link FindListOptions} for the request
* @return {Observable}
* Return an observable that contains primary bitstream information or null
*/
- public findPrimaryBitstreamByItemAndName(item: Item, bundleName: string, useCachedVersionIfAvailable = true, reRequestOnStale = true): Observable {
- return this.bundleService.findByItemAndName(item, bundleName, useCachedVersionIfAvailable, reRequestOnStale, followLink('primaryBitstream')).pipe(
+ public findPrimaryBitstreamByItemAndName(item: Item, bundleName: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, options?: FindListOptions): Observable {
+ return this.bundleService.findByItemAndName(item, bundleName, useCachedVersionIfAvailable, reRequestOnStale, options, followLink('primaryBitstream')).pipe(
getFirstCompletedRemoteData(),
switchMap((rd: RemoteData) => {
if (!rd.hasSucceeded) {
diff --git a/src/app/core/data/bundle-data.service.ts b/src/app/core/data/bundle-data.service.ts
index 5d552c9bf0..79f877fadd 100644
--- a/src/app/core/data/bundle-data.service.ts
+++ b/src/app/core/data/bundle-data.service.ts
@@ -78,10 +78,14 @@ export class BundleDataService extends IdentifiableDataService implement
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
+ * @param options the {@link FindListOptions} for the request
*/
// TODO should be implemented rest side
- findByItemAndName(item: Item, bundleName: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> {
- return this.findAllByItem(item, { elementsPerPage: 9999 }, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe(
+ findByItemAndName(item: Item, bundleName: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, options?: FindListOptions, ...linksToFollow: FollowLinkConfig[]): Observable> {
+ //Since we filter by bundleName where the pagination options are not indicated we need to load all the possible bundles.
+ // This is a workaround, in substitution of the previously recursive call with expand
+ const paginationOptions = options ?? { elementsPerPage: 9999 };
+ return this.findAllByItem(item, paginationOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe(
map((rd: RemoteData>) => {
if (hasValue(rd.payload) && hasValue(rd.payload.page)) {
const matchingBundle = rd.payload.page.find((bundle: Bundle) =>
diff --git a/src/app/core/metadata/head-tag.service.ts b/src/app/core/metadata/head-tag.service.ts
index 270e5fde72..8041bb3a4a 100644
--- a/src/app/core/metadata/head-tag.service.ts
+++ b/src/app/core/metadata/head-tag.service.ts
@@ -50,6 +50,7 @@ import { coreSelector } from '../core.selectors';
import { CoreState } from '../core-state.model';
import { BundleDataService } from '../data/bundle-data.service';
import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service';
+import { FindListOptions } from '../data/find-list-options.model';
import { PaginatedList } from '../data/paginated-list.model';
import { RemoteData } from '../data/remote-data';
import { RootDataService } from '../data/root-data.service';
@@ -331,6 +332,7 @@ export class HeadTagService {
'ORIGINAL',
true,
true,
+ new FindListOptions(),
followLink('primaryBitstream'),
followLink('bitstreams', {
findListOptions: {
diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html
index 9d8f384e16..6ac44b5ac2 100644
--- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html
+++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html
@@ -39,6 +39,9 @@
[isFirstTable]="isFirst"
aria-describedby="reorder-description">
+
+
+
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 143723d447..205c67fa61 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,4 +1,9 @@
-import { CommonModule } from '@angular/common';
+import {
+ AsyncPipe,
+ CommonModule,
+ NgForOf,
+ NgIf,
+} from '@angular/common';
import {
ChangeDetectorRef,
Component,
@@ -15,16 +20,22 @@ import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
+import { Operation } from 'fast-json-patch';
import {
+ BehaviorSubject,
combineLatest,
Observable,
Subscription,
} from 'rxjs';
import {
+ filter,
map,
switchMap,
take,
+ tap,
} from 'rxjs/operators';
+import { AlertComponent } from 'src/app/shared/alert/alert.component';
+import { AlertType } from 'src/app/shared/alert/alert-type';
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
import { BitstreamDataService } from '../../../core/data/bitstream-data.service';
@@ -40,10 +51,13 @@ import {
getFirstSucceededRemoteData,
getRemoteDataPayload,
} from '../../../core/shared/operators';
-import { AlertComponent } from '../../../shared/alert/alert.component';
-import { AlertType } from '../../../shared/alert/alert-type';
+import {
+ hasValue,
+ isNotEmpty,
+} from '../../../shared/empty.util';
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
+import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes';
import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model';
import { ObjectValuesPipe } from '../../../shared/utils/object-values-pipe';
@@ -58,10 +72,13 @@ import { ItemEditBitstreamBundleComponent } from './item-edit-bitstream-bundle/i
templateUrl: './item-bitstreams.component.html',
imports: [
CommonModule,
+ AsyncPipe,
TranslateModule,
ItemEditBitstreamBundleComponent,
RouterLink,
+ NgIf,
VarDirective,
+ NgForOf,
ThemedLoadingComponent,
AlertComponent,
],
@@ -77,9 +94,18 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
protected readonly AlertType = AlertType;
/**
- * The currently listed bundles
+ * All bundles for the current item
*/
- bundles$: Observable
;
+ private bundlesSubject = new BehaviorSubject([]);
+
+ /**
+ * The page options to use for fetching the bundles
+ */
+ bundlesOptions: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
+ id: 'bundles-pagination-options',
+ currentPage: 1,
+ pageSize: 10,
+ });
/**
* The bootstrap sizes used for the columns within this table
@@ -98,6 +124,18 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
*/
itemUpdateSubscription: Subscription;
+ /**
+ * The flag indicating to show the load more link
+ */
+ showLoadMoreLink$: BehaviorSubject = new BehaviorSubject(true);
+
+ /**
+ * The list of bundles for the current item as an observable
+ */
+ get bundles$(): Observable {
+ return this.bundlesSubject.asObservable();
+ }
+
/**
* An observable which emits a boolean which represents whether the service is currently handling a 'move' request
*/
@@ -127,14 +165,7 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
* Actions to perform after the item has been initialized
*/
postItemInit(): void {
- const bundlesOptions = this.itemBitstreamsService.getInitialBundlesPaginationOptions();
- this.isProcessingMoveRequest = this.itemBitstreamsService.getPerformingMoveRequest$();
-
- this.bundles$ = this.itemService.getBundles(this.item.id, new PaginatedSearchOptions({ pagination: bundlesOptions })).pipe(
- getFirstSucceededRemoteData(),
- getRemoteDataPayload(),
- map((bundlePage: PaginatedList) => bundlePage.page),
- );
+ this.loadBundles(1);
}
/**
@@ -199,6 +230,26 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
this.notificationsPrefix = 'item.edit.bitstreams.notifications.';
}
+ /**
+ * Load bundles for the current item
+ * @param currentPage The current page to load
+ */
+ loadBundles(currentPage?: number) {
+ this.bundlesOptions = Object.assign(new PaginationComponentOptions(), this.bundlesOptions, {
+ currentPage: currentPage || this.bundlesOptions.currentPage + 1,
+ });
+ 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]);
+ });
+ }
+
/**
* Submit the current changes
@@ -208,7 +259,7 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
submit() {
this.submitting = true;
- const removedResponses$ = this.itemBitstreamsService.removeMarkedBitstreams(this.bundles$);
+ const removedResponses$ = this.itemBitstreamsService.removeMarkedBitstreams(this.bundles$.pipe(take(1)));
// Perform the setup actions from above in order and display notifications
removedResponses$.subscribe((responses: RemoteData) => {
@@ -217,6 +268,56 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
});
}
+ /**
+ * A bitstream was dropped in a new location. Send out a Move Patch request to the REST API, display notifications,
+ * refresh the bundle's cache (so the lists can properly reload) and call the event's callback function (which will
+ * navigate the user to the correct page)
+ * @param bundle The bundle to send patch requests to
+ * @param event The event containing the index the bitstream came from and was dropped to
+ */
+ dropBitstream(bundle: Bundle, event: any) {
+ this.zone.runOutsideAngular(() => {
+ if (hasValue(event) && hasValue(event.fromIndex) && hasValue(event.toIndex) && hasValue(event.finish)) {
+ const moveOperation = {
+ op: 'move',
+ from: `/_links/bitstreams/${event.fromIndex}/href`,
+ path: `/_links/bitstreams/${event.toIndex}/href`,
+ } as Operation;
+ this.bundleService.patch(bundle, [moveOperation]).pipe(take(1)).subscribe((response: RemoteData) => {
+ this.zone.run(() => {
+ this.displayNotifications('item.edit.bitstreams.notifications.move', [response]);
+ // Remove all cached requests from this bundle and call the event's callback when the requests are cleared
+ this.requestService.removeByHrefSubstring(bundle.self).pipe(
+ filter((isCached) => isCached),
+ take(1),
+ ).subscribe(() => event.finish());
+ });
+ });
+ }
+ });
+ }
+
+ /**
+ * Display notifications
+ * - Error notification for each failed response with their message
+ * - Success notification in case there's at least one successful response
+ * @param key The i18n key for the notification messages
+ * @param responses The returned responses to display notifications for
+ */
+ displayNotifications(key: string, responses: RemoteData[]) {
+ if (isNotEmpty(responses)) {
+ const failedResponses = responses.filter((response: RemoteData) => hasValue(response) && response.hasFailed);
+ const successfulResponses = responses.filter((response: RemoteData) => hasValue(response) && response.hasSucceeded);
+
+ failedResponses.forEach((response: RemoteData) => {
+ this.notificationsService.error(this.translateService.instant(`${key}.failed.title`), response.errorMessage);
+ });
+ if (successfulResponses.length > 0) {
+ this.notificationsService.success(this.translateService.instant(`${key}.saved.title`), this.translateService.instant(`${key}.saved.content`));
+ }
+ }
+ }
+
/**
* Request the object updates service to discard all current changes to this item
* Shows a notification to remind the user that they can undo this
diff --git a/src/app/process-page/form/scripts-select/scripts-select.component.html b/src/app/process-page/form/scripts-select/scripts-select.component.html
index 9cd86d1a6d..5c161f8d8d 100644
--- a/src/app/process-page/form/scripts-select/scripts-select.component.html
+++ b/src/app/process-page/form/scripts-select/scripts-select.component.html
@@ -1,20 +1,47 @@
-
-
-
-
+
+
+
-
- {{'process.new.select-script.required' | translate}}
-
+
+ {{ 'process.new.select-script.required' | translate }}
+
+
diff --git a/src/app/process-page/form/scripts-select/scripts-select.component.scss b/src/app/process-page/form/scripts-select/scripts-select.component.scss
index e69de29bb2..4f9ca62310 100644
--- a/src/app/process-page/form/scripts-select/scripts-select.component.scss
+++ b/src/app/process-page/form/scripts-select/scripts-select.component.scss
@@ -0,0 +1,23 @@
+.dropdown-item {
+ padding: 0.35rem 1rem;
+
+ &:active {
+ color: white !important;
+ }
+}
+
+.scrollable-menu {
+ height: auto;
+ max-height: var(--ds-dropdown-menu-max-height);
+ overflow-x: hidden;
+}
+
+li:not(:last-of-type) .dropdown-item {
+ border-bottom: var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);
+}
+
+#entityControlsDropdownMenu {
+ outline: 0;
+ left: 0 !important;
+ box-shadow: var(--bs-btn-focus-box-shadow);
+}
diff --git a/src/app/process-page/form/scripts-select/scripts-select.component.spec.ts b/src/app/process-page/form/scripts-select/scripts-select.component.spec.ts
index 6b8c39eb8f..7b095926fb 100644
--- a/src/app/process-page/form/scripts-select/scripts-select.component.spec.ts
+++ b/src/app/process-page/form/scripts-select/scripts-select.component.spec.ts
@@ -87,7 +87,7 @@ describe('ScriptsSelectComponent', () => {
fixture.detectChanges();
tick();
- const select = fixture.debugElement.query(By.css('select'));
+ const select = fixture.debugElement.query(By.css('#process-script'));
select.triggerEventHandler('blur', null);
fixture.detectChanges();
@@ -101,7 +101,7 @@ describe('ScriptsSelectComponent', () => {
fixture.detectChanges();
tick();
- const select = fixture.debugElement.query(By.css('select'));
+ const select = fixture.debugElement.query(By.css('#process-script'));
select.triggerEventHandler('blur', null);
fixture.detectChanges();
diff --git a/src/app/process-page/form/scripts-select/scripts-select.component.ts b/src/app/process-page/form/scripts-select/scripts-select.component.ts
index 63c11bd91a..9eccb7ceff 100644
--- a/src/app/process-page/form/scripts-select/scripts-select.component.ts
+++ b/src/app/process-page/form/scripts-select/scripts-select.component.ts
@@ -19,32 +19,29 @@ import {
} from '@angular/forms';
import {
ActivatedRoute,
- Params,
Router,
} from '@angular/router';
+import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core';
+import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import {
- Observable,
+ BehaviorSubject,
Subscription,
} from 'rxjs';
import {
- distinctUntilChanged,
- filter,
map,
- switchMap,
- take,
+ tap,
} from 'rxjs/operators';
+import { FindListOptions } from '../../../core/data/find-list-options.model';
import { PaginatedList } from '../../../core/data/paginated-list.model';
import { ScriptDataService } from '../../../core/data/processes/script-data.service';
import {
- getFirstSucceededRemoteData,
+ getFirstCompletedRemoteData,
getRemoteDataPayload,
} from '../../../core/shared/operators';
-import {
- hasNoValue,
- hasValue,
-} from '../../../shared/empty.util';
+import { hasValue } from '../../../shared/empty.util';
+import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
import { Script } from '../../scripts/script.model';
import { controlContainerFactory } from '../process-form-factory';
@@ -61,7 +58,7 @@ const SCRIPT_QUERY_PARAMETER = 'script';
useFactory: controlContainerFactory,
deps: [[new Optional(), NgForm]] }],
standalone: true,
- imports: [NgIf, FormsModule, NgFor, AsyncPipe, TranslateModule],
+ imports: [NgIf, FormsModule, NgFor, AsyncPipe, TranslateModule, InfiniteScrollModule, ThemedLoadingComponent, NgbDropdownModule],
})
export class ScriptsSelectComponent implements OnInit, OnDestroy {
/**
@@ -71,9 +68,19 @@ export class ScriptsSelectComponent implements OnInit, OnDestroy {
/**
* All available scripts
*/
- scripts$: Observable