diff --git a/libs/ui/inputs/src/lib/checkbox/checkbox.component.css b/libs/ui/inputs/src/lib/checkbox/checkbox.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/ui/inputs/src/lib/checkbox/checkbox.component.html b/libs/ui/inputs/src/lib/checkbox/checkbox.component.html new file mode 100644 index 0000000000..947220f73f --- /dev/null +++ b/libs/ui/inputs/src/lib/checkbox/checkbox.component.html @@ -0,0 +1,8 @@ + diff --git a/libs/ui/inputs/src/lib/checkbox/checkbox.component.spec.ts b/libs/ui/inputs/src/lib/checkbox/checkbox.component.spec.ts new file mode 100644 index 0000000000..384c47f8d7 --- /dev/null +++ b/libs/ui/inputs/src/lib/checkbox/checkbox.component.spec.ts @@ -0,0 +1,47 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { CheckboxComponent } from './checkbox.component' +import { MatCheckboxModule } from '@angular/material/checkbox' + +describe('CheckboxComponent', () => { + let component: CheckboxComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [CheckboxComponent], + imports: [MatCheckboxModule], + }).compileComponents() + + fixture = TestBed.createComponent(CheckboxComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + describe('click when unchecked', () => { + beforeEach(() => { + component.checked = false + }) + it('should invert checked state when being clicked', () => { + const event = new Event('click') + component.handleClick(event) + + expect(component.checked).toBe(true) + }) + }) + + describe('click when checked', () => { + beforeEach(() => { + component.checked = true + }) + it('should invert checked state when being clicked', () => { + const event = new Event('click') + component.handleClick(event) + + expect(component.checked).toBe(false) + }) + }) +}) diff --git a/libs/ui/inputs/src/lib/checkbox/checkbox.component.stories.ts b/libs/ui/inputs/src/lib/checkbox/checkbox.component.stories.ts new file mode 100644 index 0000000000..abd0215d90 --- /dev/null +++ b/libs/ui/inputs/src/lib/checkbox/checkbox.component.stories.ts @@ -0,0 +1,46 @@ +import { + applicationConfig, + Meta, + moduleMetadata, + StoryObj, +} from '@storybook/angular' + +import { BrowserModule } from '@angular/platform-browser' +import { BrowserAnimationsModule } from '@angular/platform-browser/animations' +import { CommonModule } from '@angular/common' +import { importProvidersFrom } from '@angular/core' +import { CheckboxComponent } from './checkbox.component' +import { MatCheckbox, MatCheckboxModule } from '@angular/material/checkbox' + +export default { + title: 'Inputs/CheckboxComponent', + component: CheckboxComponent, + decorators: [ + moduleMetadata({ + declarations: [MatCheckbox], + imports: [], + }), + applicationConfig({ + providers: [ + importProvidersFrom( + BrowserModule, + BrowserAnimationsModule, + CommonModule, + MatCheckboxModule + ), + ], + }), + ], +} as Meta + +export const Primary: StoryObj = { + args: { + checked: false, + indeterminate: false, + }, + argTypes: { + changed: { + action: 'changed', + }, + }, +} diff --git a/libs/ui/inputs/src/lib/checkbox/checkbox.component.ts b/libs/ui/inputs/src/lib/checkbox/checkbox.component.ts new file mode 100644 index 0000000000..c9903a0495 --- /dev/null +++ b/libs/ui/inputs/src/lib/checkbox/checkbox.component.ts @@ -0,0 +1,25 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + Output, +} from '@angular/core' + +@Component({ + selector: 'gn-ui-checkbox', + templateUrl: './checkbox.component.html', + styleUrls: ['./checkbox.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CheckboxComponent { + @Input() checked = false + @Input() indeterminate? = false + @Output() changed = new EventEmitter() + + handleClick(event: Event) { + event.stopPropagation() + this.checked = !this.checked + this.changed.emit(this.checked) + } +} diff --git a/libs/ui/inputs/src/lib/ui-inputs.module.ts b/libs/ui/inputs/src/lib/ui-inputs.module.ts index a5c5e02445..a595dbf425 100644 --- a/libs/ui/inputs/src/lib/ui-inputs.module.ts +++ b/libs/ui/inputs/src/lib/ui-inputs.module.ts @@ -34,6 +34,7 @@ import { CheckToggleComponent } from './check-toggle/check-toggle.component' import { CopyTextButtonComponent } from './copy-text-button/copy-text-button.component' import { MatTooltipModule } from '@angular/material/tooltip' import { CommonModule } from '@angular/common' +import { CheckboxComponent } from './checkbox/checkbox.component' @NgModule({ declarations: [ @@ -58,6 +59,7 @@ import { CommonModule } from '@angular/common' FormFieldTemporalExtentComponent, CheckToggleComponent, CopyTextButtonComponent, + CheckboxComponent, ], imports: [ CommonModule, @@ -89,6 +91,7 @@ import { CommonModule } from '@angular/common' FormFieldComponent, CheckToggleComponent, CopyTextButtonComponent, + CheckboxComponent, ], }) export class UiInputsModule {} diff --git a/libs/ui/search/src/lib/record-table/record-table.component.html b/libs/ui/search/src/lib/record-table/record-table.component.html index 6343d7a8a1..809973bf98 100644 --- a/libs/ui/search/src/lib/record-table/record-table.component.html +++ b/libs/ui/search/src/lib/record-table/record-table.component.html @@ -44,14 +44,22 @@
+
+ + +
record.metadata.title @@ -75,7 +83,7 @@
record.metadata.author @@ -96,7 +104,7 @@
record.metadata.updatedOn @@ -117,7 +125,7 @@
record.metadata.createdOn @@ -141,7 +149,16 @@ (click)="recordSelect.emit(record)" *ngFor="let record of records" > -
+
+ +
+
{{ record.title }}
person {{ formatUserInfo(record.extras?.ownerInfo) }}
-
+
{{ dateToString(record.recordUpdated) }}
-
+
{{ dateToString(record.recordCreated) }}
diff --git a/libs/ui/search/src/lib/record-table/record-table.component.spec.ts b/libs/ui/search/src/lib/record-table/record-table.component.spec.ts index 142754da15..a043f39804 100644 --- a/libs/ui/search/src/lib/record-table/record-table.component.spec.ts +++ b/libs/ui/search/src/lib/record-table/record-table.component.spec.ts @@ -3,6 +3,7 @@ import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures' import { RecordTableComponent } from './record-table.component' import { SortByField } from '@geonetwork-ui/common/domain/search' +import { CatalogRecord } from '@geonetwork-ui/common/domain/record' describe('RecordTableComponent', () => { let component: RecordTableComponent @@ -15,6 +16,11 @@ describe('RecordTableComponent', () => { fixture = TestBed.createComponent(RecordTableComponent) component = fixture.componentInstance + component.records = [ + { uniqueIdentifier: '1' }, + { uniqueIdentifier: '2' }, + { uniqueIdentifier: '3' }, + ] as CatalogRecord[] fixture.detectChanges() }) @@ -76,5 +82,62 @@ describe('RecordTableComponent', () => { expect(component.isSortedBy('title', 'asc')).toBe(false) }) }) + + describe('isChecked', () => { + it('should return true when the record is in the selectedRecords array', () => { + const component = new RecordTableComponent() + component.selectedRecords = ['1', '2', '3'] + const record = { uniqueIdentifier: '2' } as any as CatalogRecord + expect(component.isChecked(record)).toBe(true) + }) + + it('should return false when the record is not in the selectedRecords array', () => { + const component = new RecordTableComponent() + component.selectedRecords = ['1', '2', '3'] + const record = { uniqueIdentifier: '4' } as any as CatalogRecord + expect(component.isChecked(record)).toBe(false) + }) + }) + + describe('#handleRecordSelectedChange', () => { + it('should add unique identifier to selectedRecords when checkbox is clicked for a record that is not already selected', () => { + const component = new RecordTableComponent() + const record = { uniqueIdentifier: '1' } + component.selectedRecords = [] + + component.handleRecordSelectedChange( + true, + record as any as CatalogRecord + ) + + expect(component.selectedRecords).toEqual(['1']) + }) + }) + + describe('#isAllSelected', () => { + it('returns true if all records are selected', () => { + component.selectedRecords = ['1', '2', '3'] + expect(component.isAllSelected()).toBe(true) + }) + it('returns false otherwise', () => { + component.selectedRecords = ['1'] + expect(component.isAllSelected()).toBe(false) + }) + }) + }) + + describe('#isSomeSelected', () => { + it('returns false if all records are selected', () => { + component.selectedRecords = ['1', '2', '3'] + expect(component.isSomeSelected()).toBe(false) + }) + it('returns true if more than one record selected', () => { + component.selectedRecords = ['2', '3'] + expect(component.isSomeSelected()).toBe(true) + }) + it('returns false if no record selected', () => { + component.selectedRecords = [] + expect(component.isSomeSelected()).toBe(false) + }) }) }) diff --git a/libs/ui/search/src/lib/record-table/record-table.component.ts b/libs/ui/search/src/lib/record-table/record-table.component.ts index 2be118f1fb..6cdf956f42 100644 --- a/libs/ui/search/src/lib/record-table/record-table.component.ts +++ b/libs/ui/search/src/lib/record-table/record-table.component.ts @@ -14,6 +14,8 @@ import { SortByField } from '@geonetwork-ui/common/domain/search' styleUrls: ['./record-table.component.css'], }) export class RecordTableComponent { + selectedRecords: string[] = [] + @Input() records: any[] = [] @Input() totalHits?: number @Input() sortBy?: SortByField @@ -90,4 +92,53 @@ export class RecordTableComponent { const sortOrder = this.getOrderForColumn(col) return sortOrder === order } + + isChecked(record: CatalogRecord) { + if (this.selectedRecords.includes(record.uniqueIdentifier)) { + return true + } + return false + } + + handleRecordSelectedChange(selected: boolean, record: CatalogRecord) { + if (!selected) { + this.selectedRecords = this.selectedRecords.filter( + (val) => val !== record.uniqueIdentifier + ) + } else { + this.selectedRecords.push(record.uniqueIdentifier) + } + } + + selectAll() { + if (this.isAllSelected()) { + this.selectedRecords = [] + } else { + this.selectedRecords = this.records.map( + (record) => record.uniqueIdentifier + ) + } + } + + isAllSelected(): boolean { + if (this.selectedRecords.length === this.records.length) { + const allRecords = this.records.filter((record) => + this.selectedRecords.includes(record.uniqueIdentifier) + ) + if (allRecords.length === this.records.length) { + return true + } + } + return false + } + + isSomeSelected(): boolean { + if ( + this.selectedRecords.length > 0 && + this.selectedRecords.length < this.records.length + ) { + return true + } + return false + } } diff --git a/libs/ui/search/src/lib/ui-search.module.ts b/libs/ui/search/src/lib/ui-search.module.ts index 4f0fecd261..3f231d3450 100644 --- a/libs/ui/search/src/lib/ui-search.module.ts +++ b/libs/ui/search/src/lib/ui-search.module.ts @@ -27,6 +27,7 @@ import { RecordPreviewFeedComponent } from './record-preview-feed/record-preview import { RecordTableComponent } from './record-table/record-table.component' import { CommonModule } from '@angular/common' import { UiInputsModule } from '@geonetwork-ui/ui/inputs' +import { MatCheckboxModule } from '@angular/material/checkbox' @NgModule({ declarations: [ @@ -56,6 +57,7 @@ import { UiInputsModule } from '@geonetwork-ui/ui/inputs' UiInputsModule, UiElementsModule, MatIconModule, + MatCheckboxModule, RouterLink, ], exports: [