diff --git a/libs/feature/record/src/lib/state/mdview.facade.ts b/libs/feature/record/src/lib/state/mdview.facade.ts
index c8aa010cbb..37364de54d 100644
--- a/libs/feature/record/src/lib/state/mdview.facade.ts
+++ b/libs/feature/record/src/lib/state/mdview.facade.ts
@@ -8,7 +8,6 @@ import { DatavizConfigurationModel } from '@geonetwork-ui/common/domain/model/da
import {
CatalogRecord,
UserFeedback,
- UserFeedbackViewModel,
} from '@geonetwork-ui/common/domain/model/record'
import { AvatarServiceInterface } from '@geonetwork-ui/api/repository'
diff --git a/libs/ui/elements/src/lib/link-card/link-card.component.html b/libs/ui/elements/src/lib/link-card/link-card.component.html
index 8f87410c66..77ecdc3f1a 100644
--- a/libs/ui/elements/src/lib/link-card/link-card.component.html
+++ b/libs/ui/elements/src/lib/link-card/link-card.component.html
@@ -1,25 +1,43 @@
-
-
- {{ link.name }}
-
-
- {{ link.description }}
-
-
- {{ link.url }}
-
-
-
- open_in_new
-
+
+
+
+ {{ link.name }}
+
+
+ {{ link.description }}
+
+
+ {{ link.url }}
+
+
+
+ open_in_new
+
+
+
+
+
+ {{ link.name || link.description }}
+
+
open_in_new
+
+
diff --git a/libs/ui/elements/src/lib/link-card/link-card.component.spec.ts b/libs/ui/elements/src/lib/link-card/link-card.component.spec.ts
index 1c84cd4b94..95bcdc690e 100644
--- a/libs/ui/elements/src/lib/link-card/link-card.component.spec.ts
+++ b/libs/ui/elements/src/lib/link-card/link-card.component.spec.ts
@@ -1,7 +1,5 @@
import { NO_ERRORS_SCHEMA } from '@angular/core'
import { ComponentFixture, TestBed } from '@angular/core/testing'
-import { MatIconModule } from '@angular/material/icon'
-import { TranslateModule } from '@ngx-translate/core'
import { LinkCardComponent } from './link-card.component'
describe('LinkCardComponent', () => {
@@ -10,9 +8,8 @@ describe('LinkCardComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
- declarations: [LinkCardComponent],
schemas: [NO_ERRORS_SCHEMA],
- imports: [MatIconModule, TranslateModule.forRoot()],
+ imports: [LinkCardComponent],
}).compileComponents()
})
diff --git a/libs/ui/elements/src/lib/link-card/link-card.component.stories.ts b/libs/ui/elements/src/lib/link-card/link-card.component.stories.ts
index 959b3cd1fe..3597360c7c 100644
--- a/libs/ui/elements/src/lib/link-card/link-card.component.stories.ts
+++ b/libs/ui/elements/src/lib/link-card/link-card.component.stories.ts
@@ -1,8 +1,3 @@
-import {
- TRANSLATE_DEFAULT_CONFIG,
- UtilI18nModule,
-} from '@geonetwork-ui/util/i18n'
-import { TranslateModule } from '@ngx-translate/core'
import {
applicationConfig,
componentWrapperDecorator,
@@ -13,18 +8,13 @@ import {
import { LinkCardComponent } from './link-card.component'
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
import { importProvidersFrom } from '@angular/core'
-import { MatIcon } from '@angular/material/icon'
export default {
title: 'Elements/LinkCardComponent',
component: LinkCardComponent,
decorators: [
moduleMetadata({
- declarations: [MatIcon],
- imports: [
- UtilI18nModule,
- TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG),
- ],
+ imports: [LinkCardComponent],
}),
applicationConfig({
providers: [importProvidersFrom(BrowserAnimationsModule)],
@@ -37,6 +27,7 @@ export default {
export const Primary: StoryObj
= {
args: {
+ compact: false,
link: {
type: 'link',
name: 'Consulter sur Géoclip',
diff --git a/libs/ui/elements/src/lib/link-card/link-card.component.ts b/libs/ui/elements/src/lib/link-card/link-card.component.ts
index 8cea45b83e..78e8bd0622 100644
--- a/libs/ui/elements/src/lib/link-card/link-card.component.ts
+++ b/libs/ui/elements/src/lib/link-card/link-card.component.ts
@@ -1,12 +1,24 @@
import { Component, ChangeDetectionStrategy, Input } from '@angular/core'
import { DatasetDistribution } from '@geonetwork-ui/common/domain/model/record'
+import { MatIconModule } from '@angular/material/icon'
+import { CommonModule } from '@angular/common'
@Component({
selector: 'gn-ui-link-card',
templateUrl: './link-card.component.html',
styleUrls: ['./link-card.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
+ standalone: true,
+ imports: [CommonModule, MatIconModule],
})
export class LinkCardComponent {
@Input() link: DatasetDistribution
+ @Input() compact = false
+
+ get title() {
+ if (this.link.name && this.link.description) {
+ return `${this.link.name} | ${this.link.description}`
+ }
+ return this.link.name || this.link.description || ''
+ }
}
diff --git a/libs/ui/elements/src/lib/ui-elements.module.ts b/libs/ui/elements/src/lib/ui-elements.module.ts
index 64373c318c..58d85ece10 100644
--- a/libs/ui/elements/src/lib/ui-elements.module.ts
+++ b/libs/ui/elements/src/lib/ui-elements.module.ts
@@ -57,7 +57,6 @@ import { TimeSincePipe } from './user-feedback-item/time-since.pipe'
DownloadItemComponent,
DownloadsListComponent,
ApiCardComponent,
- LinkCardComponent,
RelatedRecordCardComponent,
MetadataContactComponent,
MetadataCatalogComponent,
@@ -80,7 +79,6 @@ import { TimeSincePipe } from './user-feedback-item/time-since.pipe'
DownloadItemComponent,
DownloadsListComponent,
ApiCardComponent,
- LinkCardComponent,
RelatedRecordCardComponent,
MetadataContactComponent,
MetadataCatalogComponent,
diff --git a/libs/ui/inputs/src/index.ts b/libs/ui/inputs/src/index.ts
index 88f97f9216..1baf8d6f45 100644
--- a/libs/ui/inputs/src/index.ts
+++ b/libs/ui/inputs/src/index.ts
@@ -19,3 +19,4 @@ export * from './lib/text-area/text-area.component'
export * from './lib/text-input/text-input.component'
export * from './lib/ui-inputs.module'
export * from './lib/viewport-intersector/viewport-intersector.component'
+export * from './lib/previous-next-buttons/previous-next-buttons.component'
diff --git a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.css b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.css
new file mode 100644
index 0000000000..14ad317ff0
--- /dev/null
+++ b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.css
@@ -0,0 +1,6 @@
+:host {
+ --gn-ui-button-rounded: 100%;
+ --gn-ui-button-width: 8px;
+ --gn-ui-button-height: 8px;
+ --gn-ui-button-padding: 12px;
+}
diff --git a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.html b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.html
new file mode 100644
index 0000000000..d92b9276c0
--- /dev/null
+++ b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.html
@@ -0,0 +1,26 @@
+
+
+
+ arrow_back
+
+
+
+
+ arrow_forward
+
+
+
diff --git a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.spec.ts b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.spec.ts
new file mode 100644
index 0000000000..4722325bc7
--- /dev/null
+++ b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.spec.ts
@@ -0,0 +1,73 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { MatIconModule } from '@angular/material/icon'
+
+import { PreviousNextButtonsComponent } from './previous-next-buttons.component'
+import { TranslateModule } from '@ngx-translate/core'
+import { By } from '@angular/platform-browser'
+import { DebugElement } from '@angular/core'
+
+describe('PreviousNextButtonsComponent', () => {
+ let component: PreviousNextButtonsComponent
+ let fixture: ComponentFixture
+ let compiled: DebugElement
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [
+ MatIconModule,
+ PreviousNextButtonsComponent,
+ TranslateModule.forRoot(),
+ ],
+ }).compileComponents()
+ })
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(PreviousNextButtonsComponent)
+ component = fixture.componentInstance
+ compiled = fixture.debugElement
+ })
+
+ it('should create', () => {
+ expect(component).toBeTruthy()
+ })
+
+ describe('onFirstElement', () => {
+ beforeEach(() => {
+ component.isFirst = true
+ component.isLast = false
+ fixture.detectChanges()
+ })
+
+ it('previous button should be disabled', () => {
+ const previousButton = compiled.query(
+ By.css('[data-test="previousButton"]')
+ )
+ expect(previousButton.attributes['ng-reflect-disabled']).toEqual('true')
+ })
+
+ it("next button shouldn't be disabled", () => {
+ const nextButton = compiled.query(By.css('[data-test="nextButton"]'))
+ expect(nextButton.attributes['ng-reflect-disabled']).toEqual('false')
+ })
+ })
+
+ describe('onLastElement', () => {
+ beforeEach(() => {
+ component.isFirst = false
+ component.isLast = true
+ fixture.detectChanges()
+ })
+
+ it('previous button should be disabled', () => {
+ const previousButton = compiled.query(
+ By.css('[data-test="previousButton"]')
+ )
+ expect(previousButton.attributes['ng-reflect-disabled']).toEqual('false')
+ })
+
+ it("next button shouldn't be disabled", () => {
+ const nextButton = compiled.query(By.css('[data-test="nextButton"]'))
+ expect(nextButton.attributes['ng-reflect-disabled']).toEqual('true')
+ })
+ })
+})
diff --git a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.stories.ts b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.stories.ts
new file mode 100644
index 0000000000..efc14c7116
--- /dev/null
+++ b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.stories.ts
@@ -0,0 +1,39 @@
+import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'
+import { PreviousNextButtonsComponent } from './previous-next-buttons.component'
+import { TranslateModule } from '@ngx-translate/core'
+import { MatIconModule } from '@angular/material/icon'
+import {
+ TRANSLATE_DEFAULT_CONFIG,
+ UtilI18nModule,
+} from '@geonetwork-ui/util/i18n'
+
+export default {
+ title: 'Inputs/PreviousNextButtonsComponent',
+ component: PreviousNextButtonsComponent,
+ parameters: {
+ backgrounds: {
+ default: 'dark',
+ },
+ },
+ decorators: [
+ moduleMetadata({
+ imports: [
+ UtilI18nModule,
+ TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG),
+ MatIconModule,
+ ],
+ }),
+ ],
+} as Meta
+
+export const Primary: StoryObj = {
+ args: {
+ isFirst: true,
+ isLast: false,
+ },
+ render: (args) => ({
+ props: args,
+ template:
+ '',
+ }),
+}
diff --git a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.ts b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.ts
new file mode 100644
index 0000000000..3546968262
--- /dev/null
+++ b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.ts
@@ -0,0 +1,32 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ EventEmitter,
+ Input,
+ Output,
+} from '@angular/core'
+import { ButtonComponent } from '../button/button.component'
+import { MatIconModule } from '@angular/material/icon'
+
+@Component({
+ selector: 'gn-ui-previous-next-buttons',
+ templateUrl: './previous-next-buttons.component.html',
+ styleUrls: ['./previous-next-buttons.component.css'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ standalone: true,
+ imports: [ButtonComponent, MatIconModule],
+})
+export class PreviousNextButtonsComponent {
+ @Input() isFirst: boolean
+ @Input() isLast: boolean
+
+ @Output() directionButtonClicked: EventEmitter = new EventEmitter()
+
+ previousButtonClicked() {
+ this.directionButtonClicked.next('previous')
+ }
+
+ nextButtonClicked() {
+ this.directionButtonClicked.next('next')
+ }
+}
diff --git a/libs/ui/layout/src/index.ts b/libs/ui/layout/src/index.ts
index 9dbf8aec71..358c081b9f 100644
--- a/libs/ui/layout/src/index.ts
+++ b/libs/ui/layout/src/index.ts
@@ -6,4 +6,5 @@ export * from './lib/form-field-wrapper/form-field-wrapper.component'
export * from './lib/interactive-table/interactive-table-column/interactive-table-column.component'
export * from './lib/interactive-table/interactive-table.component'
export * from './lib/sticky-header/sticky-header.component'
+export * from './lib/block-list/block-list.component'
export * from './lib/ui-layout.module'
diff --git a/libs/ui/layout/src/lib/block-list/block-list.component.css b/libs/ui/layout/src/lib/block-list/block-list.component.css
new file mode 100644
index 0000000000..0d37f18585
--- /dev/null
+++ b/libs/ui/layout/src/lib/block-list/block-list.component.css
@@ -0,0 +1,23 @@
+:host .block-list-container ::ng-deep > * {
+ flex-shrink: 0;
+}
+
+:host {
+ position: relative;
+}
+
+.list-page-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 6px;
+ position: relative;
+}
+
+.list-page-dot:after {
+ content: '';
+ position: absolute;
+ left: -7px;
+ top: -7px;
+ width: 20px;
+ height: 20px;
+}
diff --git a/libs/ui/layout/src/lib/block-list/block-list.component.html b/libs/ui/layout/src/lib/block-list/block-list.component.html
new file mode 100644
index 0000000000..17e706ddd6
--- /dev/null
+++ b/libs/ui/layout/src/lib/block-list/block-list.component.html
@@ -0,0 +1,20 @@
+
+
+
+ 1"
+ class="absolute flex flex-row justify-center gap-[14px] p-1"
+ [ngClass]="paginationContainerClass"
+>
+
+
diff --git a/libs/ui/layout/src/lib/block-list/block-list.component.spec.ts b/libs/ui/layout/src/lib/block-list/block-list.component.spec.ts
new file mode 100644
index 0000000000..2b28278dde
--- /dev/null
+++ b/libs/ui/layout/src/lib/block-list/block-list.component.spec.ts
@@ -0,0 +1,173 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { BlockListComponent } from './block-list.component'
+import { Component, Input } from '@angular/core'
+import { By } from '@angular/platform-browser'
+
+@Component({
+ template: `
+
+ `,
+})
+class BlockListWrapperComponent {
+ @Input() blocks = [1, 2, 3, 4, 5, 6, 7]
+}
+
+describe('BlockListComponent', () => {
+ let component: BlockListComponent
+ let fixture: ComponentFixture
+ let blockEls: HTMLElement[]
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [BlockListWrapperComponent],
+ imports: [BlockListComponent],
+ }).compileComponents()
+ fixture = TestBed.createComponent(BlockListWrapperComponent)
+ component = fixture.debugElement.query(
+ By.directive(BlockListComponent)
+ ).componentInstance
+ fixture.detectChanges()
+ blockEls = fixture.debugElement
+ .queryAll(By.css('.block'))
+ .map((el) => el.nativeElement)
+ })
+
+ it('should create', () => {
+ expect(component).toBeTruthy()
+ })
+
+ describe('pages computation', () => {
+ it('shows 5 items per page initially', () => {
+ const blocksVisibility = blockEls.map((el) => el.style.display !== 'none')
+ expect(blocksVisibility).toEqual([
+ true,
+ true,
+ true,
+ true,
+ true,
+ false,
+ false,
+ ])
+ })
+ describe('click on step', () => {
+ beforeEach(() => {
+ component.goToPage(1)
+ })
+ it('updates visibility', () => {
+ const blocksVisibility = blockEls.map(
+ (el) => el.style.display !== 'none'
+ )
+ expect(blocksVisibility).toEqual([
+ false,
+ false,
+ false,
+ false,
+ false,
+ true,
+ true,
+ ])
+ })
+ it('emits the selected page', () => {
+ expect(component['currentPage']).toEqual(1)
+ })
+ })
+ describe('custom page size', () => {
+ beforeEach(() => {
+ component.pageSize = 3
+ component.goToPage(3)
+ fixture.detectChanges()
+ })
+ it('updates visibility', () => {
+ const blocksVisibility = blockEls.map(
+ (el) => el.style.display !== 'none'
+ )
+ expect(blocksVisibility).toEqual([
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ true,
+ ])
+ })
+ })
+ })
+
+ describe('previousPage', () => {
+ beforeEach(() => {
+ component.pageSize = 2
+ component.goToPage(2)
+ component.previousPage()
+ })
+ it('changes to previous page', () => {
+ expect(component['currentPage']).toEqual(1)
+ })
+ })
+
+ describe('nextPage', () => {
+ beforeEach(() => {
+ component.pageSize = 2
+ component.goToPage(1)
+ component.nextPage()
+ })
+ it('changes to next page', () => {
+ expect(component['currentPage']).toEqual(2)
+ })
+ })
+
+ describe('isFirstPage', () => {
+ beforeEach(() => {
+ component.pageSize = 3
+ })
+ it('returns true if the current page is the first one', () => {
+ expect(component.isFirstPage).toBe(true)
+ })
+ it('returns false if the current page is not the first one', () => {
+ component.goToPage(1)
+ expect(component.isFirstPage).toBe(false)
+ })
+ })
+
+ describe('isLastPage', () => {
+ beforeEach(() => {
+ component.pageSize = 3
+ })
+ it('returns true if the current page is the last one', () => {
+ component.goToPage(2)
+ expect(component.isLastPage).toBe(true)
+ })
+ it('returns false if the current page is not the last one', () => {
+ component.goToPage(1)
+ expect(component.isLastPage).toBe(false)
+ })
+ })
+
+ describe('set initial height as min height, keeps value when height changes', () => {
+ beforeEach(() => {
+ Object.defineProperties(component.blockContainer.nativeElement, {
+ clientHeight: {
+ value: 150,
+ },
+ })
+ fixture.detectChanges()
+ component.ngAfterViewInit()
+ Object.defineProperties(component.blockContainer.nativeElement, {
+ clientHeight: {
+ value: 50,
+ },
+ })
+ fixture.detectChanges()
+ })
+ it('sets the min height of the container according to its initial content', () => {
+ expect(component.blockContainer.nativeElement.style.minHeight).toBe(
+ '150px'
+ )
+ })
+ })
+})
diff --git a/libs/ui/layout/src/lib/block-list/block-list.component.stories.ts b/libs/ui/layout/src/lib/block-list/block-list.component.stories.ts
new file mode 100644
index 0000000000..31f06fdd74
--- /dev/null
+++ b/libs/ui/layout/src/lib/block-list/block-list.component.stories.ts
@@ -0,0 +1,44 @@
+import type { Meta, StoryObj } from '@storybook/angular'
+import { BlockListComponent } from './block-list.component'
+import { componentWrapperDecorator } from '@storybook/angular'
+
+const meta: Meta = {
+ component: BlockListComponent,
+ title: 'Layout/BlockListComponent',
+ decorators: [
+ componentWrapperDecorator(
+ (story) =>
+ `${story}
`
+ ),
+ ],
+}
+export default meta
+type Story = StoryObj<
+ BlockListComponent & {
+ blockCount: number
+ }
+>
+
+export const Primary: Story = {
+ args: {
+ pageSize: 5,
+ blockCount: 17,
+ },
+ render: (args) => ({
+ props: {
+ ...args,
+ blockList: new Array(args.blockCount).fill(0).map((_, i) => i + 1),
+ },
+ template: `
+
+
+ Box {{ block }}
+
+
+`,
+ }),
+}
diff --git a/libs/ui/layout/src/lib/block-list/block-list.component.ts b/libs/ui/layout/src/lib/block-list/block-list.component.ts
new file mode 100644
index 0000000000..5a0937c761
--- /dev/null
+++ b/libs/ui/layout/src/lib/block-list/block-list.component.ts
@@ -0,0 +1,84 @@
+import {
+ AfterViewInit,
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ ContentChildren,
+ ElementRef,
+ Input,
+ QueryList,
+ ViewChild,
+} from '@angular/core'
+import { CommonModule } from '@angular/common'
+
+@Component({
+ selector: 'gn-ui-block-list',
+ templateUrl: './block-list.component.html',
+ styleUrls: ['./block-list.component.css'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ standalone: true,
+ imports: [CommonModule],
+})
+export class BlockListComponent implements AfterViewInit {
+ @Input() pageSize = 5
+ @Input() containerClass = ''
+ @Input() paginationContainerClass = 'w-full bottom-0 top-auto'
+ @ContentChildren('block', { read: ElementRef }) blocks: QueryList<
+ ElementRef
+ >
+ @ViewChild('blockContainer') blockContainer: ElementRef
+
+ protected minHeight = 0
+
+ protected currentPage = 0
+ protected get pages() {
+ return new Array(this.pagesCount).fill(0).map((_, i) => i)
+ }
+
+ get isFirstPage() {
+ return this.currentPage === 0
+ }
+ get isLastPage() {
+ return this.currentPage === this.pagesCount - 1
+ }
+ get pagesCount() {
+ return this.blocks ? Math.ceil(this.blocks.length / this.pageSize) : 1
+ }
+
+ constructor(private changeDetector: ChangeDetectorRef) {}
+
+ ngAfterViewInit() {
+ this.blocks.changes.subscribe(this.refreshBlocksVisibility)
+ this.refreshBlocksVisibility()
+
+ // we store the first height as the min-height of the list container
+ this.minHeight = this.blockContainer.nativeElement.clientHeight
+ this.changeDetector.detectChanges()
+ }
+
+ protected refreshBlocksVisibility = () => {
+ this.blocks.forEach((block, index) => {
+ block.nativeElement.style.display =
+ index >= this.currentPage * this.pageSize &&
+ index < (this.currentPage + 1) * this.pageSize
+ ? null
+ : 'none'
+ })
+ }
+
+ public goToPage(index: number) {
+ this.currentPage = Math.max(Math.min(index, this.pagesCount - 1), 0)
+ this.changeDetector.detectChanges()
+ this.refreshBlocksVisibility()
+ }
+
+ public previousPage() {
+ if (this.isFirstPage) return
+ this.goToPage(this.currentPage - 1)
+ }
+
+ public nextPage() {
+ if (this.isLastPage) return
+ this.goToPage(this.currentPage + 1)
+ }
+}
diff --git a/libs/ui/layout/src/lib/carousel/carousel.component.css b/libs/ui/layout/src/lib/carousel/carousel.component.css
index ed93f493c4..e751dea020 100644
--- a/libs/ui/layout/src/lib/carousel/carousel.component.css
+++ b/libs/ui/layout/src/lib/carousel/carousel.component.css
@@ -1,20 +1,23 @@
:host .carousel-container ::ng-deep > * {
flex-shrink: 0;
}
+
:host {
position: relative;
}
+
.carousel-step-dot {
width: 6px;
height: 6px;
border-radius: 6px;
position: relative;
}
+
.carousel-step-dot:after {
content: '';
position: absolute;
- left: -4px;
- top: -4px;
- width: 14px;
- height: 14px;
+ left: -7px;
+ top: -7px;
+ width: 20px;
+ height: 20px;
}
diff --git a/libs/ui/layout/src/lib/carousel/carousel.component.html b/libs/ui/layout/src/lib/carousel/carousel.component.html
index 52034ab817..0c2ae552d9 100644
--- a/libs/ui/layout/src/lib/carousel/carousel.component.html
+++ b/libs/ui/layout/src/lib/carousel/carousel.component.html
@@ -1,17 +1,17 @@
-
-
+
1"
- class="absolute right-0 top-0 flex flex-row justify-center gap-[10px] p-1"
+ class="absolute flex flex-row justify-center gap-[14px] p-1"
[ngClass]="stepsContainerClass"
>
diff --git a/libs/ui/layout/src/lib/carousel/carousel.component.spec.ts b/libs/ui/layout/src/lib/carousel/carousel.component.spec.ts
index d691b6c08d..7301cb0d1a 100644
--- a/libs/ui/layout/src/lib/carousel/carousel.component.spec.ts
+++ b/libs/ui/layout/src/lib/carousel/carousel.component.spec.ts
@@ -34,7 +34,7 @@ jest.mock('embla-carousel', () => {
})
@Component({
- template: `
+ template: `
`,
})
@@ -49,7 +49,8 @@ describe('CarouselComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
- declarations: [CarouselWrapperComponent, CarouselComponent],
+ declarations: [CarouselWrapperComponent],
+ imports: [CarouselComponent],
}).compileComponents()
fixture = TestBed.createComponent(CarouselWrapperComponent)
component = fixture.debugElement.query(
@@ -68,7 +69,7 @@ describe('CarouselComponent', () => {
})
it('computes steps initially', () => {
expect(component.steps).toEqual([0, 0.5, 0.75, 1])
- expect(component.selectedStep).toEqual(0)
+ expect(component.currentStep).toEqual(0)
})
describe('click on step', () => {
beforeEach(() => {
@@ -78,8 +79,39 @@ describe('CarouselComponent', () => {
expect(component.emblaApi.scrollTo).toHaveBeenCalledWith(2)
})
it('sets the clicked step as selected', () => {
- expect(component.selectedStep).toEqual(2)
+ expect(component.currentStep).toEqual(2)
})
})
})
+
+ describe('currentStepChange', () => {
+ it('emits the current step index', () => {
+ const spy = jest.fn()
+ component.currentStepChange.subscribe(spy)
+ component.scrollToStep(2)
+ expect(spy).toHaveBeenCalledWith(2)
+ expect(spy).toHaveBeenCalledTimes(1)
+ })
+ })
+
+ describe('isFirstStep', () => {
+ it('returns true if the current step is the first one', () => {
+ expect(component.isFirstStep).toBe(true)
+ })
+ it('returns false if the current step is not the first one', () => {
+ component.scrollToStep(2)
+ expect(component.isFirstStep).toBe(false)
+ })
+ })
+
+ describe('isLastStep', () => {
+ it('returns true if the current step is the last one', () => {
+ component.scrollToStep(3)
+ expect(component.isLastStep).toBe(true)
+ })
+ it('returns false if the current step is not the last one', () => {
+ component.scrollToStep(1)
+ expect(component.isLastStep).toBe(false)
+ })
+ })
})
diff --git a/libs/ui/layout/src/lib/carousel/carousel.component.stories.ts b/libs/ui/layout/src/lib/carousel/carousel.component.stories.ts
index 6086a98635..c24a9d5a04 100644
--- a/libs/ui/layout/src/lib/carousel/carousel.component.stories.ts
+++ b/libs/ui/layout/src/lib/carousel/carousel.component.stories.ts
@@ -8,7 +8,8 @@ const meta: Meta = {
decorators: [
componentWrapperDecorator(
(story) =>
- `${story}
`
+ `Please note that the carousel will overflow by default; to hide its items, make its container overflow-hidden!
+${story}
`
),
],
}
@@ -20,15 +21,15 @@ export const Primary: Story = {
render: (args) => ({
props: args,
template: `
-
+
First box
Second box
-
- Third box
+
+ Third box (resize me!)
Fourth box
diff --git a/libs/ui/layout/src/lib/carousel/carousel.component.ts b/libs/ui/layout/src/lib/carousel/carousel.component.ts
index 0d9c52897f..abb751af2a 100644
--- a/libs/ui/layout/src/lib/carousel/carousel.component.ts
+++ b/libs/ui/layout/src/lib/carousel/carousel.component.ts
@@ -4,25 +4,49 @@ import {
ChangeDetectorRef,
Component,
ElementRef,
+ EventEmitter,
Input,
+ Output,
ViewChild,
} from '@angular/core'
import EmblaCarousel, { EmblaCarouselType } from 'embla-carousel'
+import { CommonModule } from '@angular/common'
@Component({
selector: 'gn-ui-carousel',
templateUrl: './carousel.component.html',
styleUrls: ['./carousel.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
+ standalone: true,
+ imports: [CommonModule],
})
export class CarouselComponent implements AfterViewInit {
+ @ViewChild('carouselOverflowContainer') carouselOverflowContainer: ElementRef
+
@Input() containerClass = ''
- @Input() stepsContainerClass = ''
- @ViewChild('carouselOverflowContainer')
- carouselOverflowContainer: ElementRef
- steps: number[] = []
- selectedStep = -1
- emblaApi: EmblaCarouselType
+ @Input() stepsContainerClass = 'w-full bottom-0 top-auto'
+ @Output() currentStepChange = new EventEmitter()
+
+ protected steps: number[] = []
+ protected emblaApi: EmblaCarouselType
+ protected currentStep = 0
+
+ protected refreshSteps = () => {
+ this.steps = this.emblaApi.scrollSnapList()
+ this.currentStep = this.emblaApi.selectedScrollSnap()
+ this.currentStepChange.emit(this.currentStep)
+ this.changeDetector.detectChanges()
+ }
+
+ get isFirstStep() {
+ return this.currentStep === 0
+ }
+ get isLastStep() {
+ return this.currentStep === this.steps.length - 1
+ }
+ get stepsCount() {
+ return this.steps.length
+ }
constructor(private changeDetector: ChangeDetectorRef) {}
@@ -33,18 +57,24 @@ export class CarouselComponent implements AfterViewInit {
duration: 15,
}
)
- const refreshSteps = () => {
- this.steps = this.emblaApi.scrollSnapList()
- this.selectedStep = this.emblaApi.selectedScrollSnap()
- this.changeDetector.detectChanges()
- }
+
this.emblaApi
- .on('init', refreshSteps)
- .on('reInit', refreshSteps)
- .on('select', refreshSteps)
+ .on('init', this.refreshSteps)
+ .on('reInit', this.refreshSteps)
+ .on('select', this.refreshSteps)
}
- scrollToStep(stepIndex: number) {
+ public scrollToStep(stepIndex: number) {
this.emblaApi.scrollTo(stepIndex)
}
+
+ public slideToPrevious() {
+ if (this.isFirstStep) return
+ this.emblaApi.scrollPrev()
+ }
+
+ public slideToNext() {
+ if (this.isLastStep) return
+ this.emblaApi.scrollNext()
+ }
}
diff --git a/libs/ui/layout/src/lib/ui-layout.module.ts b/libs/ui/layout/src/lib/ui-layout.module.ts
index 69a28b9c8b..5d22fc9ce9 100644
--- a/libs/ui/layout/src/lib/ui-layout.module.ts
+++ b/libs/ui/layout/src/lib/ui-layout.module.ts
@@ -15,14 +15,12 @@ import { CarouselComponent } from './carousel/carousel.component'
StickyHeaderComponent,
AnchorLinkDirective,
ExpandablePanelButtonComponent,
- CarouselComponent,
],
exports: [
ExpandablePanelComponent,
StickyHeaderComponent,
AnchorLinkDirective,
ExpandablePanelButtonComponent,
- CarouselComponent,
],
})
export class UiLayoutModule {}
diff --git a/tools/e2e/commands.ts b/tools/e2e/commands.ts
index b3b5ec3cc6..0c06e446e3 100644
--- a/tools/e2e/commands.ts
+++ b/tools/e2e/commands.ts
@@ -35,18 +35,33 @@ Cypress.Commands.add(
},
})
cy.getCookie('XSRF-TOKEN').then((xsrfTokenCookie) => {
- cy.request({
- method: 'POST',
- url: '/geonetwork/signin',
- body: `username=${username}&password=${password}&_csrf=${xsrfTokenCookie.value}`,
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- },
- followRedirect: false,
- })
+ // do the login 2 times because it sometimes doesn't register (?)
+ for (let i = 0; i < 2; i++) {
+ cy.request({
+ method: 'POST',
+ url: '/geonetwork/signin',
+ body: `username=${username}&password=${password}&_csrf=${xsrfTokenCookie.value}`,
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ followRedirect: false,
+ })
+ }
+ })
+ cy.request({
+ method: 'GET',
+ url: '/geonetwork/srv/api/me',
+ headers: {
+ Accept: 'application/json',
+ },
+ }).then((response) => {
+ if (response.status !== 200) {
+ throw new Error('Could not log in to GeoNetwork API 😢')
+ }
+ cy.log('Login to GeoNetwork API successful!')
})
- if (redirect) return cy.visit('/')
- else return cy.window()
+ if (redirect) cy.visit('/')
+ return cy.window()
}
)