Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ME: Add checkbox for records table #661

Merged
merged 4 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
8 changes: 8 additions & 0 deletions libs/ui/inputs/src/lib/checkbox/checkbox.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<mat-checkbox
class="cursor-pointer"
type="checkbox"
[checked]="checked"
[indeterminate]="indeterminate"
(click)="handleClick($event)"
color="primary"
></mat-checkbox>
47 changes: 47 additions & 0 deletions libs/ui/inputs/src/lib/checkbox/checkbox.component.spec.ts
Original file line number Diff line number Diff line change
@@ -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<CheckboxComponent>

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)
})
})
})
46 changes: 46 additions & 0 deletions libs/ui/inputs/src/lib/checkbox/checkbox.component.stories.ts
Original file line number Diff line number Diff line change
@@ -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<CheckboxComponent>

export const Primary: StoryObj<CheckboxComponent> = {
args: {
checked: false,
indeterminate: false,
},
argTypes: {
changed: {
action: 'changed',
},
},
}
25 changes: 25 additions & 0 deletions libs/ui/inputs/src/lib/checkbox/checkbox.component.ts
Original file line number Diff line number Diff line change
@@ -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<boolean>()

handleClick(event: Event) {
event.stopPropagation()
this.checked = !this.checked
this.changed.emit(this.checked)
}
}
3 changes: 3 additions & 0 deletions libs/ui/inputs/src/lib/ui-inputs.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand All @@ -58,6 +59,7 @@ import { CommonModule } from '@angular/common'
FormFieldTemporalExtentComponent,
CheckToggleComponent,
CopyTextButtonComponent,
CheckboxComponent,
],
imports: [
CommonModule,
Expand Down Expand Up @@ -89,6 +91,7 @@ import { CommonModule } from '@angular/common'
FormFieldComponent,
CheckToggleComponent,
CopyTextButtonComponent,
CheckboxComponent,
],
})
export class UiInputsModule {}
33 changes: 25 additions & 8 deletions libs/ui/search/src/lib/record-table/record-table.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,22 @@
</div>

<div
class="grid grid-cols-[repeat(5,minmax(0,max-content))] gap-x-4 gap-y-1"
class="grid grid-cols-[repeat(6,minmax(0,max-content))] gap-x-4 gap-y-1"
*ngIf="!records[0].name"
>
<div class="contents text-sm">
<div class="flex justify-center items-center">
<gn-ui-checkbox
[checked]="isAllSelected()"
[indeterminate]="isSomeSelected()"
(changed)="selectAll()"
>
</gn-ui-checkbox>
</div>
<div class="record-table-header text-gray-400 flex gap-1">
<gn-ui-button
type="light"
extraClass="px-3 py-2 space-x-1"
extraClass="px-3 pl-0 space-x-1 text-left"
(buttonClick)="setSortBy('resourceTitleObject.default.keyword')"
>
<span translate>record.metadata.title</span>
Expand All @@ -75,7 +83,7 @@
<div class="record-table-header text-gray-400 flex gap-1">
<gn-ui-button
type="light"
extraClass="px-3 py-2 space-x-1"
extraClass="px-3 pl-0 space-x-1"
(buttonClick)="setSortBy('recordOwner')"
>
<span translate>record.metadata.author</span>
Expand All @@ -96,7 +104,7 @@
<div class="record-table-header text-gray-400 flex gap-1">
<gn-ui-button
type="light"
extraClass="px-3 py-2 space-x-1"
extraClass="px-3 pl-0 space-x-1"
(buttonClick)="setSortBy('changeDate')"
>
<span translate>record.metadata.updatedOn</span>
Expand All @@ -117,7 +125,7 @@
<div class="record-table-header text-gray-400 flex gap-1">
<gn-ui-button
type="light"
extraClass="px-3 py-2 space-x-1"
extraClass="px-3 pl-0 space-x-1"
(buttonClick)="setSortBy('createDate')"
>
<span translate>record.metadata.createdOn</span>
Expand All @@ -141,7 +149,16 @@
(click)="recordSelect.emit(record)"
*ngFor="let record of records"
>
<div [title]="record.title" class="record-table-col text-16">
<div class="record-table-col">
<gn-ui-checkbox
[checked]="isChecked(record)"
(changed)="handleRecordSelectedChange($event, record)"
></gn-ui-checkbox>
</div>
<div
[title]="record.title"
class="record-table-col text-16 self-center"
>
{{ record.title }}
</div>
<div
Expand Down Expand Up @@ -174,10 +191,10 @@
<mat-icon class="material-symbols-outlined"> person </mat-icon>
<span class="">{{ formatUserInfo(record.extras?.ownerInfo) }}</span>
</div>
<div class="record-table-col text-16">
<div class="record-table-col text-16 self-center">
{{ dateToString(record.recordUpdated) }}
</div>
<div class="record-table-col text-16">
<div class="record-table-col text-16 self-center">
{{ dateToString(record.recordCreated) }}
</div>
</div>
Expand Down
63 changes: 63 additions & 0 deletions libs/ui/search/src/lib/record-table/record-table.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
})

Expand Down Expand Up @@ -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)
})
})
})
51 changes: 51 additions & 0 deletions libs/ui/search/src/lib/record-table/record-table.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
2 changes: 2 additions & 0 deletions libs/ui/search/src/lib/ui-search.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down Expand Up @@ -56,6 +57,7 @@ import { UiInputsModule } from '@geonetwork-ui/ui/inputs'
UiInputsModule,
UiElementsModule,
MatIconModule,
MatCheckboxModule,
RouterLink,
],
exports: [
Expand Down
Loading