Skip to content

Commit

Permalink
feat: optionally place additional actions in overflow menu (#298)
Browse files Browse the repository at this point in the history
* feat: implement conditional additionalActions for data table

* feat: implement conditional additionalActions for data list grid

* feat: add showAsOverFlow functionality to data table

* feat: add showAsOverFlow functionality to data list grid

* fix: add missing translations + fix linter error

* fix: add missing permission check

* fix: fix linter
bastianjakobi authored Jun 28, 2024
1 parent fb5ddc9 commit 27f0f2d
Showing 11 changed files with 220 additions and 39 deletions.
1 change: 1 addition & 0 deletions libs/angular-accelerator/assets/i18n/de.json
Original file line number Diff line number Diff line change
@@ -55,6 +55,7 @@
"SHOWING": "{{first}} - {{last}} von {{totalRecords}}",
"SHOWING_WITH_TOTAL_ON_SERVER": "{{first}} - {{last}} von {{totalRecords}} ({{totalRecordsOnServer}})",
"ALL": "Alle",
"MORE_ACTIONS": "Weitere Aktionen",
"ACTIONS": {
"VIEW": "Anzeigen",
"EDIT": "Bearbeiten",
1 change: 1 addition & 0 deletions libs/angular-accelerator/assets/i18n/en.json
Original file line number Diff line number Diff line change
@@ -55,6 +55,7 @@
"SHOWING": "{{first}} - {{last}} of {{totalRecords}}",
"SHOWING_WITH_TOTAL_ON_SERVER": "{{first}} - {{last}} of {{totalRecords}} ({{totalRecordsOnServer}})",
"ALL": "All",
"MORE_ACTIONS": "More actions",
"ACTIONS": {
"VIEW": "View",
"EDIT": "Edit",
Original file line number Diff line number Diff line change
@@ -124,7 +124,7 @@
[attr.name]="'data-list-action-button'"
></button>
</ng-container>
<ng-container *ngFor="let action of additionalActions">
<ng-container *ngFor="let action of additionalListActions">
<ng-container *ngIf="(!action.actionVisibleField || fieldIsTruthy(item, action.actionVisibleField))">
<button
*ocxIfPermission="action.permission"
@@ -139,6 +139,17 @@
></button>
</ng-container>
</ng-container>
<ng-container *ngIf="hasVisibleOverflowMenuItems(item)">
<p-menu #menu [model]="(getOverflowMenuItems(item) | async) || []" [popup]="true" appendTo="body"></p-menu>
<button
pButton
class="p-button-rounded p-button-text"
[icon]="'pi pi-ellipsis-v'"
(click)="menu.toggle($event)"
[ariaLabel]="'OCX_DATA_TABLE.MORE_ACTIONS' | translate"
[title]="'OCX_DATA_TABLE.MORE_ACTIONS' | translate"
></button>
</ng-container>
</div>
</div>
<div class="text-base font-light my-1">
Original file line number Diff line number Diff line change
@@ -154,6 +154,39 @@ export const ListWithConditionallyVisibleAdditionalActions = {
},
}

export const ListWithAdditionalOverflowActions = {
argTypes: defaultArgTypes,
render: Template,
args: {
...defaultComponentArgs,
additionalActions: [
{
id: '1',
labelKey: 'Additional Action',
icon: 'pi pi-plus',
permission: 'TEST_MGMT#TEST_VIEW',
showAsOverflow: true,
},
{
id: '2',
labelKey: 'Conditionally Hidden',
icon: 'pi pi-plus',
permission: 'TEST_MGMT#TEST_VIEW',
showAsOverflow: true,
actionVisibleField: 'available',
},
{
id: '3',
labelKey: 'Conditionally Enabled',
icon: 'pi pi-plus',
permission: 'TEST_MGMT#TEST_VIEW',
showAsOverflow: true,
actionEnabledField: 'available',
},
]
},
}

export const GridWithMockData = {
render: Template,
argTypes: defaultArgTypes,
Original file line number Diff line number Diff line change
@@ -149,6 +149,8 @@ export class DataListGridComponent extends DataSortBase implements OnInit, DoChe
return this.gridItemTemplate || this.gridItemChildTemplate
}

additionalListActions: DataAction[] = []
additionalListOverflowActions: DataAction[] = []
_additionalActions: DataAction[] = []
@Input()
get additionalActions(): DataAction[] {
@@ -157,6 +159,8 @@ export class DataListGridComponent extends DataSortBase implements OnInit, DoChe
set additionalActions(value: DataAction[]) {
this._additionalActions = value
this.updateGridMenuItems()
this.additionalListActions = value.filter((action) => !action.showAsOverflow)
this.additionalListOverflowActions = value.filter((action) => action.showAsOverflow)
}

@Output() viewItem = new EventEmitter<ListGridData>()
@@ -253,22 +257,26 @@ export class DataListGridComponent extends DataSortBase implements OnInit, DoChe
}

updateGridMenuItems(useSelectedItem = false): void {
let deleteDisabled = false;
let editDisabled = false;
let viewDisabled = false;

let deleteVisible = true;
let editVisible = true;
let viewVisible = true;

if(useSelectedItem && this.selectedItem) {
viewDisabled = !!this.viewActionEnabledField && !this.fieldIsTruthy(this.selectedItem, this.viewActionEnabledField);
editDisabled = !!this.editActionEnabledField && !this.fieldIsTruthy(this.selectedItem, this.editActionEnabledField);
deleteDisabled = !!this.deleteActionEnabledField && !this.fieldIsTruthy(this.selectedItem, this.deleteActionEnabledField);

viewVisible = (!this.viewActionVisibleField || this.fieldIsTruthy(this.selectedItem, this.viewActionVisibleField))
editVisible = (!this.editActionVisibleField || this.fieldIsTruthy(this.selectedItem, this.editActionVisibleField))
deleteVisible = (!this.deleteActionVisibleField || this.fieldIsTruthy(this.selectedItem, this.deleteActionVisibleField))
let deleteDisabled = false
let editDisabled = false
let viewDisabled = false

let deleteVisible = true
let editVisible = true
let viewVisible = true

if (useSelectedItem && this.selectedItem) {
viewDisabled =
!!this.viewActionEnabledField && !this.fieldIsTruthy(this.selectedItem, this.viewActionEnabledField)
editDisabled =
!!this.editActionEnabledField && !this.fieldIsTruthy(this.selectedItem, this.editActionEnabledField)
deleteDisabled =
!!this.deleteActionEnabledField && !this.fieldIsTruthy(this.selectedItem, this.deleteActionEnabledField)

viewVisible = !this.viewActionVisibleField || this.fieldIsTruthy(this.selectedItem, this.viewActionVisibleField)
editVisible = !this.editActionVisibleField || this.fieldIsTruthy(this.selectedItem, this.editActionVisibleField)
deleteVisible =
!this.deleteActionVisibleField || this.fieldIsTruthy(this.selectedItem, this.deleteActionVisibleField)
}

this.translateService
@@ -289,7 +297,7 @@ export class DataListGridComponent extends DataSortBase implements OnInit, DoChe
command: () => this.viewItem.emit(this.selectedItem),
disabled: viewDisabled,
visible: viewVisible,
automationId: viewVisible ? automationId : automationIdHidden
automationId: viewVisible ? automationId : automationIdHidden,
})
}
if (this.editItem.observed && this.userService.hasPermission(this.editPermission || '')) {
@@ -299,7 +307,7 @@ export class DataListGridComponent extends DataSortBase implements OnInit, DoChe
command: () => this.editItem.emit(this.selectedItem),
disabled: editDisabled,
visible: editVisible,
automationId: editVisible ? automationId : automationIdHidden
automationId: editVisible ? automationId : automationIdHidden,
})
}
if (this.deleteItem.observed && this.userService.hasPermission(this.deletePermission || '')) {
@@ -309,7 +317,7 @@ export class DataListGridComponent extends DataSortBase implements OnInit, DoChe
command: () => this.deleteItem.emit(this.selectedItem),
disabled: deleteDisabled,
visible: deleteVisible,
automationId: deleteVisible ? automationId : automationIdHidden
automationId: deleteVisible ? automationId : automationIdHidden,
})
}
menuItems = menuItems.concat(
@@ -319,7 +327,8 @@ export class DataListGridComponent extends DataSortBase implements OnInit, DoChe
label: translations[a.labelKey || ''],
icon: a.icon,
styleClass: (a.classes || []).join(' '),
disabled: a.disabled || (!!a.actionEnabledField && !this.fieldIsTruthy(this.selectedItem, a.actionEnabledField)),
disabled:
a.disabled || (!!a.actionEnabledField && !this.fieldIsTruthy(this.selectedItem, a.actionEnabledField)),
visible: !a.actionVisibleField || this.fieldIsTruthy(this.selectedItem, a.actionVisibleField),
command: () => a.callback(this.selectedItem),
}))
@@ -345,4 +354,29 @@ export class DataListGridComponent extends DataSortBase implements OnInit, DoChe
fieldIsTruthy(object: any, key: any) {
return !!this.resolveFieldData(object, key)
}

hasVisibleOverflowMenuItems(item: any) {
return this.additionalListOverflowActions.some(
(a) =>
(!a.actionVisibleField || this.fieldIsTruthy(item, a.actionVisibleField)) &&
this.userService.hasPermission(a.permission)
)
}

getOverflowMenuItems(item: any) {
return this.translateService.get([...this.additionalListOverflowActions.map((a) => a.labelKey || '')]).pipe(
map((translations) => {
return this.additionalListOverflowActions
.filter((a) => this.userService.hasPermission(a.permission))
.map((a) => ({
label: translations[a.labelKey || ''],
icon: a.icon,
styleClass: (a.classes || []).join(' '),
disabled: a.disabled || (!!a.actionEnabledField && !this.fieldIsTruthy(item, a.actionEnabledField)),
visible: !a.actionVisibleField || this.fieldIsTruthy(item, a.actionVisibleField),
command: () => a.callback(item),
}))
})
)
}
}
Original file line number Diff line number Diff line change
@@ -9,7 +9,9 @@
[ngClass]="(frozenActionColumn && actionColumnPosition === 'left') ? 'border-right-1' : (frozenActionColumn && actionColumnPosition === 'right') ? 'border-left-1' : ''"
>
<div class="icon-button-row-wrapper">
<ng-container *ngIf="viewTableRowObserved && (!viewActionVisibleField || fieldIsTruthy(rowObject, viewActionVisibleField))">
<ng-container
*ngIf="viewTableRowObserved && (!viewActionVisibleField || fieldIsTruthy(rowObject, viewActionVisibleField))"
>
<button
*ocxIfPermission="viewPermission"
[disabled]="!!viewActionEnabledField && !fieldIsTruthy(rowObject, viewActionEnabledField)"
@@ -22,7 +24,9 @@
[attr.name]="'data-table-action-button'"
></button>
</ng-container>
<ng-container *ngIf="editTableRowObserved && (!editActionVisibleField || fieldIsTruthy(rowObject, editActionVisibleField))">
<ng-container
*ngIf="editTableRowObserved && (!editActionVisibleField || fieldIsTruthy(rowObject, editActionVisibleField))"
>
<button
*ocxIfPermission="editPermission"
[disabled]="!!editActionEnabledField && !fieldIsTruthy(rowObject, editActionEnabledField)"
@@ -35,7 +39,9 @@
[attr.name]="'data-table-action-button'"
></button>
</ng-container>
<ng-container *ngIf="deleteTableRowObserved && (!deleteActionVisibleField || fieldIsTruthy(rowObject, deleteActionVisibleField))">
<ng-container
*ngIf="deleteTableRowObserved && (!deleteActionVisibleField || fieldIsTruthy(rowObject, deleteActionVisibleField))"
>
<button
*ocxIfPermission="deletePermission"
[disabled]="!!deleteActionEnabledField && !fieldIsTruthy(rowObject, deleteActionEnabledField)"
@@ -49,20 +55,33 @@
></button>
</ng-container>
<ng-container *ngFor="let action of additionalActions">
<ng-container *ngIf="(!action.actionVisibleField || fieldIsTruthy(rowObject, action.actionVisibleField))">
<ng-container
*ngIf="(!action.actionVisibleField || fieldIsTruthy(rowObject, action.actionVisibleField))"
>
<button
*ocxIfPermission="action.permission"
pButton
class="p-button-rounded p-button-text"
[ngClass]="action.classes"
[icon]="action.icon || ''"
(click)="action.callback(rowObject)"
[title]="action.labelKey ? (action.labelKey | translate) : ''"
[attr.aria-label]="action.labelKey ? (action.labelKey | translate) : ''"
[disabled]="action.disabled || (!!action.actionEnabledField && !fieldIsTruthy(rowObject, action.actionEnabledField))"
></button>
</ng-container>
</ng-container>
<ng-container *ngIf="hasVisibleOverflowMenuItems(rowObject)">
<p-menu #menu [model]="(getOverflowMenuItems(rowObject) | async) || []" [popup]="true" appendTo="body"></p-menu>
<button
*ocxIfPermission="action.permission"
pButton
class="p-button-rounded p-button-text"
[ngClass]="action.classes"
[icon]="action.icon || ''"
(click)="action.callback(rowObject)"
[title]="action.labelKey ? (action.labelKey | translate) : ''"
[attr.aria-label]="action.labelKey ? (action.labelKey | translate) : ''"
[disabled]="action.disabled || (!!action.actionEnabledField && !fieldIsTruthy(rowObject, action.actionEnabledField))"
[icon]="'pi pi-ellipsis-v'"
(click)="menu.toggle($event)"
[ariaLabel]="'OCX_DATA_TABLE.MORE_ACTIONS' | translate"
[title]="'OCX_DATA_TABLE.MORE_ACTIONS' | translate"
></button>
</ng-container>
</ng-container>
</div>
</td>
</ng-container>
@@ -189,7 +208,9 @@
</ng-template>
<ng-template pTemplate="emptymessage">
<tr>
<td [colSpan]="columns.length + ((anyRowActionObserved || this.additionalActions.length > 0) ? 1 : 0)">{{ emptyResultsMessage || ("OCX_DATA_TABLE.EMPTY_RESULT" | translate) }}</td>
<td [colSpan]="columns.length + ((anyRowActionObserved || this.additionalActions.length > 0) ? 1 : 0)">
{{ emptyResultsMessage || ("OCX_DATA_TABLE.EMPTY_RESULT" | translate) }}
</td>
</tr>
</ng-template>
</p-table>
@@ -279,8 +300,7 @@
<ng-container> {{ rowObject[column.id] | number }} </ng-container>
</ng-template>

<ng-template #defaultCustomCell let-rowObject="rowObject" let-column="column">
</ng-template>
<ng-template #defaultCustomCell let-rowObject="rowObject" let-column="column"> </ng-template>

<ng-template #defaultDateCell let-rowObject="rowObject" let-column="column">
<ng-container> {{ rowObject[column.id] | date: 'medium' }} </ng-container>
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ import { StorybookTranslateModule } from './../../storybook-translate.module'
import { MockAuthModule } from '../../mock-auth/mock-auth.module'
import { IfPermissionDirective } from '../../directives/if-permission.directive'
import { ColumnType } from '../../model/column-type.model'
import { MenuModule } from 'primeng/menu'

type DataTableInputTypes = Pick<DataTableComponent, 'rows' | 'columns' | 'emptyResultsMessage' | 'selectedRows'>
const DataTableComponentSBConfig: Meta<DataTableComponent> = {
@@ -27,7 +28,7 @@ const DataTableComponentSBConfig: Meta<DataTableComponent> = {
}),
moduleMetadata({
declarations: [DataTableComponent, IfPermissionDirective],
imports: [TableModule, ButtonModule, MultiSelectModule, StorybookTranslateModule, MockAuthModule],
imports: [TableModule, ButtonModule, MultiSelectModule, StorybookTranslateModule, MockAuthModule, MenuModule],
}),
],
}
@@ -355,4 +356,47 @@ export const WithConditionallyVisibleAdditionalActions = {
},
}

export const WithAdditionalOverflowActions = {
argTypes: {
deleteTableRow: { action: 'deleteTableRow' },
editTableRow: { action: 'deleteTableRow' },
viewTableRow: { action: 'deleteTableRow' },
},
render: Template,
args: {
...defaultComponentArgs,
deleteTableRow: ($event: any) => console.log('Delete table row ', $event),
editTableRow: ($event: any) => console.log('Edit table row ', $event),
viewTableRow: ($event: any) => console.log('View table row ', $event),
deletePermission: 'TEST_MGMT#TEST_DELETE',
editPermission: 'TEST_MGMT#TEST_EDIT',
viewPermission: 'TEST_MGMT#TEST_VIEW',
additionalActions: [
{
id: '1',
labelKey: 'Additional Action',
icon: 'pi pi-plus',
permission: 'TEST_MGMT#TEST_VIEW',
showAsOverflow: true,
},
{
id: '2',
labelKey: 'Conditionally Hidden',
icon: 'pi pi-plus',
permission: 'TEST_MGMT#TEST_VIEW',
showAsOverflow: true,
actionVisibleField: 'available',
},
{
id: '3',
labelKey: 'Conditionally Enabled',
icon: 'pi pi-plus',
permission: 'TEST_MGMT#TEST_VIEW',
showAsOverflow: true,
actionEnabledField: 'available',
},
]
},
}

export default DataTableComponentSBConfig
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@ import { DataSortDirection } from '../../model/data-sort-direction'
import { DataTableColumn } from '../../model/data-table-column.model'
import { DataSortBase } from '../data-sort-base/data-sort-base'
import { ObjectUtils } from '../../utils/objectutils'
import { UserService } from '@onecx/angular-integration-interface'

type Primitive = number | string | boolean | bigint | Date
export type Row = {
@@ -154,7 +155,15 @@ export class DataTableComponent extends DataSortBase implements OnInit {
return this.translationKeyCellTemplate || this.translationKeyCellChildTemplate
}

@Input() additionalActions: DataAction[] = []
_additionalActions: DataAction[] = []
@Input()
get additionalActions(): DataAction[] {
return this._additionalActions
}
set additionalActions(value: DataAction[]) {
this._additionalActions = value.filter((action) => !action.showAsOverflow)
this.overflowActions = value.filter((action) => action.showAsOverflow)
}
@Input() frozenActionColumn = false
@Input() actionColumnPosition: 'left' | 'right' = 'right'

@@ -173,6 +182,9 @@ export class DataTableComponent extends DataSortBase implements OnInit {
currentFilterOptions$: Observable<SelectItem[]> | undefined
currentSelectedFilters$: Observable<string[]> | undefined
filterAmounts$: Observable<Record<string, number>> | undefined

overflowActions: DataAction[] = []

get viewTableRowObserved(): boolean {
const dv = this.injector.get('DataViewComponent', null)
return dv?.viewItemObserved || dv?.viewItem.observed || this.viewTableRow.observed
@@ -198,7 +210,8 @@ export class DataTableComponent extends DataSortBase implements OnInit {
@Inject(LOCALE_ID) locale: string,
translateService: TranslateService,
private router: Router,
private injector: Injector
private injector: Injector,
private userService: UserService
) {
super(locale, translateService)
this.name = this.name || this.router.url.replace(/[^A-Za-z0-9]/, '_')
@@ -349,4 +362,25 @@ export class DataTableComponent extends DataSortBase implements OnInit {
fieldIsTruthy(object: any, key: any) {
return !!ObjectUtils.resolveFieldData(object, key)
}

hasVisibleOverflowMenuItems(row: any) {
return this.overflowActions.some((a) => !a.actionVisibleField || this.fieldIsTruthy(row, a.actionVisibleField))
}

getOverflowMenuItems(row: any) {
return this.translateService.get([...this.overflowActions.map((a) => a.labelKey || '')]).pipe(
map((translations) => {
return this.overflowActions
.filter((a) => this.userService.hasPermission(a.permission))
.map((a) => ({
label: translations[a.labelKey || ''],
icon: a.icon,
styleClass: (a.classes || []).join(' '),
disabled: a.disabled || (!!a.actionEnabledField && !this.fieldIsTruthy(row, a.actionEnabledField)),
visible: !a.actionVisibleField || this.fieldIsTruthy(row, a.actionVisibleField),
command: () => a.callback(row),
}))
})
)
}
}
1 change: 1 addition & 0 deletions libs/angular-accelerator/src/lib/model/data-action.ts
Original file line number Diff line number Diff line change
@@ -7,5 +7,6 @@ export interface DataAction {
disabled?: boolean
actionVisibleField?: string
actionEnabledField?: string
showAsOverflow?: boolean
callback: (data: any) => void
}
1 change: 1 addition & 0 deletions libs/portal-integration-angular/assets/i18n/de.json
Original file line number Diff line number Diff line change
@@ -76,6 +76,7 @@
"SHOWING": "{{first}} - {{last}} von {{totalRecords}}",
"SHOWING_WITH_TOTAL_ON_SERVER": "{{first}} - {{last}} von {{totalRecords}} ({{totalRecordsOnServer}})",
"ALL": "Alle",
"MORE_ACTIONS": "Weitere Aktionen",
"ACTIONS": {
"VIEW": "Anzeigen",
"EDIT": "Bearbeiten",
1 change: 1 addition & 0 deletions libs/portal-integration-angular/assets/i18n/en.json
Original file line number Diff line number Diff line change
@@ -76,6 +76,7 @@
"SHOWING": "{{first}} - {{last}} of {{totalRecords}}",
"SHOWING_WITH_TOTAL_ON_SERVER": "{{first}} - {{last}} of {{totalRecords}} ({{totalRecordsOnServer}})",
"ALL": "All",
"MORE_ACTIONS": "More actions",
"ACTIONS": {
"VIEW": "View",
"EDIT": "Edit",

0 comments on commit 27f0f2d

Please sign in to comment.