UI for editing customizable dashboard views

This commit is contained in:
shamoon 2024-04-17 00:23:51 -07:00
parent 23a96810f7
commit 4cd9fd6832
10 changed files with 281 additions and 31 deletions

View File

@ -120,6 +120,7 @@ import { RotateConfirmDialogComponent } from './components/common/confirm-dialog
import { MergeConfirmDialogComponent } from './components/common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component'
import { SplitConfirmDialogComponent } from './components/common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component'
import { DocumentHistoryComponent } from './components/document-history/document-history.component'
import { DragDropSelectComponent } from './components/common/input/drag-drop-select/drag-drop-select.component'
import {
airplane,
archive,
@ -474,6 +475,7 @@ function initializeApp(settings: SettingsService) {
MergeConfirmDialogComponent,
SplitConfirmDialogComponent,
DocumentHistoryComponent,
DragDropSelectComponent,
],
imports: [
BrowserModule,

View File

@ -320,17 +320,14 @@
</div>
<h4 i18n>Views</h4>
<div formGroupName="savedViews">
<ul class="list-group" formGroupName="savedViews">
@for (view of savedViews; track view) {
<li class="list-group-item py-3">
<div [formGroupName]="view.id" class="row">
<div class="mb-3 col">
<label class="form-label" for="name_{{view.id}}" i18n>Name</label>
<input type="text" class="form-control" formControlName="name" id="name_{{view.id}}">
</div>
<div class="mb-2 col">
<label class="form-label" for="show_on_dashboard_{{view.id}}" i18n>&nbsp;<span class="visually-hidden">Appears on</span></label>
<div class="form-check form-switch">
<div class="col">
<pngx-input-text title="Name" formControlName="name"></pngx-input-text>
<div class="form-check form-switch mt-3">
<input type="checkbox" class="form-check-input" id="show_on_dashboard_{{view.id}}" formControlName="show_on_dashboard">
<label class="form-check-label" for="show_on_dashboard_{{view.id}}" i18n>Show on dashboard</label>
</div>
@ -339,7 +336,26 @@
<label class="form-check-label" for="show_in_sidebar_{{view.id}}" i18n>Show in sidebar</label>
</div>
</div>
<div class="mb-2 col-auto">
<div class="col">
@if (savedViewGroup.get(view.id.toString()).get('show_on_dashboard').value) {
<div class="row">
<div class="col">
<pngx-input-number i18n-title title="Limit documents" [showAdd]="false" formControlName="dashboard_view_limit"></pngx-input-number>
</div>
<div class="col">
<label class="form-label" for="dashboard_view_mode_{{view.id}}" i18n>Display as</label>
<select class="form-select" formControlName="dashboard_view_mode">
<option [ngValue]="DashboardViewMode.TABLE" i18n>Table</option>
<option [ngValue]="DashboardViewMode.SMALL_CARDS" i18n>Cards</option>
</select>
</div>
</div>
@if (savedViewGroup.get(view.id.toString()).get('dashboard_view_mode').value === DashboardViewMode.TABLE) {
<pngx-input-drag-drop-select [items]="DASHBOARD_VIEW_TABLE_COLUMNS" formControlName="dashboard_view_table_columns"></pngx-input-drag-drop-select>
}
}
</div>
<div class="col-auto">
<label class="form-label" for="name_{{view.id}}" i18n>Actions</label>
<pngx-confirm-button
@ -352,20 +368,23 @@
</pngx-confirm-button>
</div>
</div>
</li>
}
@if (savedViews && savedViews.length === 0) {
<div i18n>No saved views defined.</div>
<li class="list-group-item">
<div i18n>No saved views defined.</div>
</li>
}
@if (!savedViews) {
<div>
<li class="list-group-item">
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div>
</div>
</li>
}
</div>
</ul>
</ng-template>
</li>
@ -374,4 +393,5 @@
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
<button type="submit" class="btn btn-primary mb-2" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
<button type="button" (click)="reset()" class="btn btn-secondary ms-2 mb-2" [disabled]="(isDirty$ | async) === false" i18n>Cancel</button>
</form>

View File

@ -437,4 +437,11 @@ describe('SettingsComponent', () => {
size: 'xl',
})
})
it('should support reset', () => {
completeSetup()
component.settingsForm.get('themeColor').setValue('#ff0000')
component.reset()
expect(component.settingsForm.get('themeColor').value).toEqual('')
})
})

View File

@ -26,7 +26,12 @@ import {
tap,
} from 'rxjs'
import { Group } from 'src/app/data/group'
import { SavedView } from 'src/app/data/saved-view'
import {
DASHBOARD_VIEW_TABLE_COLUMNS,
DashboardViewMode,
DashboardViewTableColumn,
SavedView,
} from 'src/app/data/saved-view'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { User } from 'src/app/data/user'
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
@ -73,8 +78,8 @@ export class SettingsComponent
extends ComponentWithPermissions
implements OnInit, AfterViewInit, OnDestroy, DirtyComponent
{
SettingsNavIDs = SettingsNavIDs
activeNavID: number
DashboardViewMode = DashboardViewMode
savedViewGroup = new FormGroup({})
@ -110,6 +115,8 @@ export class SettingsComponent
})
savedViews: SavedView[]
SettingsNavIDs = SettingsNavIDs
DASHBOARD_VIEW_TABLE_COLUMNS = DASHBOARD_VIEW_TABLE_COLUMNS
store: BehaviorSubject<any>
storeSub: Subscription
@ -340,6 +347,9 @@ export class SettingsComponent
name: view.name,
show_on_dashboard: view.show_on_dashboard,
show_in_sidebar: view.show_in_sidebar,
dashboard_view_limit: view.dashboard_view_limit,
dashboard_view_mode: view.dashboard_view_mode,
dashboard_view_table_columns: view.dashboard_view_table_columns,
}
this.savedViewGroup.addControl(
view.id.toString(),
@ -348,6 +358,9 @@ export class SettingsComponent
name: new FormControl(null),
show_on_dashboard: new FormControl(null),
show_in_sidebar: new FormControl(null),
dashboard_view_limit: new FormControl(null),
dashboard_view_mode: new FormControl(null),
dashboard_view_table_columns: new FormControl([]),
})
)
}
@ -592,6 +605,10 @@ export class SettingsComponent
}
}
reset() {
this.settingsForm.patchValue(this.store.getValue())
}
clearThemeColor() {
this.settingsForm.get('themeColor').patchValue('')
}

View File

@ -0,0 +1,24 @@
<div class="d-flex flex-row mt-2 align-items-center">
<ng-container i18n>Selected</ng-container>:
<div class="ms-2 d-flex flex-row gap-2 w-100"
cdkDropList #selectedList="cdkDropList"
cdkDropListOrientation="horizontal"
(cdkDropListDropped)="drop($event)"
[cdkDropListConnectedTo]="[unselectedList]">
@for (item of selectedItems; track item.id) {
<div class="badge bg-primary" cdkDrag>{{item.name}}</div>
}
</div>
</div>
<div class="d-flex flex-row mt-2 align-items-center bg-light p-2">
<ng-container i18n></ng-container>
<div class="d-flex flex-row gap-2 w-100"
cdkDropList #unselectedList="cdkDropList"
cdkDropListOrientation="horizontal"
(cdkDropListDropped)="drop($event)"
[cdkDropListConnectedTo]="[selectedList]">
@for (item of unselectedItems; track item.id) {
<div class="badge bg-secondary opacity-50" cdkDrag>{{item.name}}</div>
}
</div>
</div>

View File

@ -0,0 +1,3 @@
.badge {
cursor: move;
}

View File

@ -0,0 +1,99 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { DragDropModule } from '@angular/cdk/drag-drop'
import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'
import { DragDropSelectComponent } from './drag-drop-select.component'
describe('DragDropSelectComponent', () => {
let component: DragDropSelectComponent
let fixture: ComponentFixture<DragDropSelectComponent>
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DragDropModule, FormsModule],
declarations: [DragDropSelectComponent],
}).compileComponents()
fixture = TestBed.createComponent(DragDropSelectComponent)
component = fixture.componentInstance
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
fixture.detectChanges()
})
it('should update selectedItems when writeValue is called', () => {
const newValue = ['1', '2', '3']
component.items = [
{ id: '1', name: 'Item 1' },
{ id: '2', name: 'Item 2' },
{ id: '3', name: 'Item 3' },
]
component.writeValue(newValue)
expect(component.selectedItems).toEqual([
{ id: '1', name: 'Item 1' },
{ id: '2', name: 'Item 2' },
{ id: '3', name: 'Item 3' },
])
})
it('should update selectedItems when an item is dropped within selectedList', () => {
component.items = [
{ id: '1', name: 'Item 1' },
{ id: '2', name: 'Item 2' },
{ id: '3', name: 'Item 3' },
{ id: '4', name: 'Item 4' },
]
component.writeValue(['1', '2', '3'])
const event = {
previousContainer: component.selectedList,
container: component.selectedList,
previousIndex: 1,
currentIndex: 2,
}
component.drop(event as any)
expect(component.selectedItems).toEqual([
{ id: '1', name: 'Item 1' },
{ id: '3', name: 'Item 3' },
{ id: '2', name: 'Item 2' },
])
})
it('should update selectedItems when an item is dropped from unselectedList to selectedList', () => {
component.items = [
{ id: '1', name: 'Item 1' },
{ id: '2', name: 'Item 2' },
{ id: '3', name: 'Item 3' },
]
component.writeValue(['1', '2'])
const event = {
previousContainer: component.unselectedList,
container: component.selectedList,
previousIndex: 0,
currentIndex: 2,
}
component.drop(event as any)
expect(component.selectedItems).toEqual([
{ id: '1', name: 'Item 1' },
{ id: '2', name: 'Item 2' },
{ id: '3', name: 'Item 3' },
])
})
it('should update selectedItems when an item is dropped from selectedList to unselectedList', () => {
component.items = [
{ id: '1', name: 'Item 1' },
{ id: '2', name: 'Item 2' },
{ id: '3', name: 'Item 3' },
]
component.writeValue(['1', '2', '3'])
const event = {
previousContainer: component.selectedList,
container: component.unselectedList,
previousIndex: 1,
currentIndex: 0,
}
component.drop(event as any)
expect(component.selectedItems).toEqual([
{ id: '1', name: 'Item 1' },
{ id: '3', name: 'Item 3' },
])
})
})

View File

@ -0,0 +1,61 @@
import { Component, Input, ViewChild, forwardRef } from '@angular/core'
import { NG_VALUE_ACCESSOR } from '@angular/forms'
import { AbstractInputComponent } from '../abstract-input'
import {
CdkDragDrop,
CdkDropList,
moveItemInArray,
} from '@angular/cdk/drag-drop'
@Component({
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DragDropSelectComponent),
multi: true,
},
],
selector: 'pngx-input-drag-drop-select',
templateUrl: './drag-drop-select.component.html',
styleUrl: './drag-drop-select.component.scss',
})
export class DragDropSelectComponent extends AbstractInputComponent<string[]> {
@Input() items: { id: string; name: string }[] = []
public selectedItems: { id: string; name: string }[] = []
@ViewChild('selectedList') selectedList: CdkDropList
@ViewChild('unselectedList') unselectedList: CdkDropList
get unselectedItems(): { id: string; name: string }[] {
return this.items.filter((i) => !this.selectedItems.includes(i))
}
writeValue(newValue: string[]): void {
super.writeValue(newValue)
this.selectedItems = newValue.map((id) =>
this.items.find((i) => i.id === id)
)
}
public drop(event: CdkDragDrop<string[]>) {
if (
event.previousContainer === event.container &&
event.container === this.selectedList
) {
moveItemInArray(
this.selectedItems,
event.previousIndex,
event.currentIndex
)
} else if (event.container === this.selectedList) {
this.selectedItems.splice(
event.currentIndex,
0,
this.unselectedItems[event.previousIndex]
)
} else {
this.selectedItems.splice(event.previousIndex, 1)
}
this.onChange(this.selectedItems.map((i) => i.id))
}
}

View File

@ -13,6 +13,7 @@ import {
DashboardViewTableColumn,
DashboardViewMode,
SavedView,
DASHBOARD_VIEW_TABLE_COLUMNS,
} from 'src/app/data/saved-view'
import { ConsumerStatusService } from 'src/app/services/consumer-status.service'
import { DocumentService } from 'src/app/services/rest/document.service'
@ -223,21 +224,6 @@ export class SavedViewWidgetComponent
}
public getColumnTitle(column: DashboardViewTableColumn): string {
switch (column) {
case DashboardViewTableColumn.TITLE:
return $localize`Title`
case DashboardViewTableColumn.CREATED:
return $localize`Created`
case DashboardViewTableColumn.ADDED:
return $localize`Added`
case DashboardViewTableColumn.TAGS:
return $localize`Tags`
case DashboardViewTableColumn.CORRESPONDENT:
return $localize`Correspondent`
case DashboardViewTableColumn.DOCUMENT_TYPE:
return $localize`Document type`
case DashboardViewTableColumn.STORAGE_PATH:
return $localize`Storage path`
}
return DASHBOARD_VIEW_TABLE_COLUMNS.find((c) => c.id === column)?.name
}
}

View File

@ -16,6 +16,37 @@ export enum DashboardViewTableColumn {
STORAGE_PATH = 'storagepath',
}
export const DASHBOARD_VIEW_TABLE_COLUMNS = [
{
id: DashboardViewTableColumn.TITLE,
name: $localize`Title`,
},
{
id: DashboardViewTableColumn.CREATED,
name: $localize`Created`,
},
{
id: DashboardViewTableColumn.ADDED,
name: $localize`Added`,
},
{
id: DashboardViewTableColumn.TAGS,
name: $localize`Tags`,
},
{
id: DashboardViewTableColumn.CORRESPONDENT,
name: $localize`Correspondent`,
},
{
id: DashboardViewTableColumn.DOCUMENT_TYPE,
name: $localize`Document type`,
},
{
id: DashboardViewTableColumn.STORAGE_PATH,
name: $localize`Storage path`,
},
]
export interface SavedView extends ObjectWithPermissions {
name?: string