UI for editing customizable dashboard views
This commit is contained in:
parent
23a96810f7
commit
4cd9fd6832
@ -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,
|
||||
|
@ -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> <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) {
|
||||
<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>
|
||||
|
@ -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('')
|
||||
})
|
||||
})
|
||||
|
@ -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('')
|
||||
}
|
||||
|
@ -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>
|
@ -0,0 +1,3 @@
|
||||
.badge {
|
||||
cursor: move;
|
||||
}
|
@ -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' },
|
||||
])
|
||||
})
|
||||
})
|
@ -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))
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user