WEB-528 Improve the data visualization in Data Tables (#2942)

This commit is contained in:
Soni Jay
2025-12-28 14:29:40 -06:00
committed by GitHub
parent 7b2fe03e90
commit cb52f59dc3
7 changed files with 265 additions and 106 deletions
+10 -9
View File
@@ -199,15 +199,16 @@ export class Datatables {
}
}
// Convert snake_case to Title Case (e.g., "created_at" -> "Created At")
// For non-CODELOOKUP fields, only filter standalone "cd" if it appears as an artifact
return (
columnName
.split('_')
.filter((word) => word.trim() && word.toLowerCase() !== 'cd') // Remove empty strings and standalone "cd" artifacts
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ') || columnName
); // Fallback to original if all filtered out
if (columnName.includes('_')) {
return (
columnName
.split('_')
.filter((word) => word.trim() && word.toLowerCase() !== 'cd')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ') || columnName
);
}
return columnName;
}
public getCodeLookupValue(columnHeader: any, id: number): string {
@@ -32,33 +32,35 @@
</div>
</div>
<div class="m-t-10">
<table #dataTable mat-table [dataSource]="datatableData" class="mat-elevation-z1 m-b-25">
@for (datatableColumn of datatableColumns; track datatableColumn; let i = $index) {
<ng-container [matColumnDef]="datatableColumn">
@if (i === 0) {
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let data">
<mat-checkbox
class="center"
(click)="$event.stopPropagation()"
(change)="$event ? itemToggle(data) : null"
[checked]="selection.isSelected(data)"
>
</mat-checkbox>
</td>
}
@if (i > 0) {
<th mat-header-cell class="right" *matHeaderCellDef>{{ getInputName(datatableColumn) }}</th>
<td mat-cell class="right" *matCellDef="let data" [ngClass]="isToDelete(data)">
{{ formatValue(data, datatableColumn) }}
</td>
}
</ng-container>
}
<mat-card class="data-table-card m-t-10">
<mat-card-content>
<table #dataTable mat-table [dataSource]="datatableData" class="mat-elevation-z2 m-b-25">
@for (datatableColumn of datatableColumns; track datatableColumn; let i = $index) {
<ng-container [matColumnDef]="datatableColumn">
@if (i === 0) {
<th mat-header-cell *matHeaderCellDef class="checkbox-column"></th>
<td mat-cell *matCellDef="let data">
<mat-checkbox
class="center"
(click)="$event.stopPropagation()"
(change)="$event ? itemToggle(data) : null"
[checked]="selection.isSelected(data)"
>
</mat-checkbox>
</td>
}
@if (i > 0) {
<th mat-header-cell class="right" *matHeaderCellDef>{{ getInputName(datatableColumn) }}</th>
<td mat-cell class="right" *matCellDef="let data" [ngClass]="isToDelete(data)">
{{ formatValue(data, datatableColumn) }}
</td>
}
</ng-container>
}
<tr mat-header-row *matHeaderRowDef="datatableColumns"></tr>
<tr mat-row *matRowDef="let row; columns: datatableColumns"></tr>
</table>
</div>
<tr mat-header-row *matHeaderRowDef="datatableColumns"></tr>
<tr mat-row *matRowDef="let row; columns: datatableColumns" class="data-row"></tr>
</table>
</mat-card-content>
</mat-card>
</div>
@@ -9,12 +9,65 @@
margin-left: auto;
}
.data-table-card {
margin-top: 16px;
box-shadow: 0 2px 4px rgb(0 0 0 / 10%);
.mat-card-content {
padding: 0;
}
}
table {
width: 100%;
border-radius: 4px;
overflow: hidden;
th {
background-color: rgb(0 0 0 / 4%);
font-weight: 600;
padding: 16px 24px;
border-bottom: 2px solid rgb(0 0 0 / 12%);
font-size: 0.875rem;
letter-spacing: 0.5px;
text-transform: uppercase;
}
td {
padding: 16px 24px;
border-bottom: 1px solid rgb(0 0 0 / 6%);
font-size: 0.9375rem;
}
.data-row {
transition: background-color 0.2s ease;
cursor: pointer;
&:hover {
background-color: rgb(0 0 0 / 3%);
}
}
.checkbox-column {
max-width: 60px;
text-align: center;
}
.mat-column-select {
max-width: 60px;
}
}
}
.tobe-deleted {
text-decoration: line-through;
color: $orange;
opacity: 0.7;
background-color: rgb(255 152 0 / 10%);
}
::ng-deep .mat-checkbox {
.mat-checkbox-frame {
border-width: 2px;
}
}
@@ -16,6 +16,7 @@ import {
MatRow
} from '@angular/material/table';
import { ActivatedRoute } from '@angular/router';
import { MatCard, MatCardContent } from '@angular/material/card';
import { Datatables } from 'app/core/utils/datatables';
import { Dates } from 'app/core/utils/dates';
import { DateFormatPipe } from 'app/pipes/date-format.pipe';
@@ -36,6 +37,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
imports: [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
MatCard,
MatCardContent,
MatTable,
MatColumnDef,
MatHeaderCellDef,
@@ -21,76 +21,86 @@
<mat-divider></mat-divider>
@if (dataObject.data.length > 0) {
<div class="m-t-10">
@for (columnHeader of dataObject.columnHeaders; track columnHeader; let i = $index) {
<div class="flex-fill" [ngClass]="setAttributeClass(columnHeader.columnName)">
<div class="mat-body-strong left flex-40">
{{ datatables.toDisplayLabel(columnHeader.columnName) }}
</div>
<div class="right flex-60">
@switch (getColumnType(columnHeader.columnDisplayType, columnHeader.columnType)) {
@case ('DATE') {
<span>
{{ dataObject.data[0].row[i] | dateFormat }}
</span>
}
@case ('DATETIME') {
<span>
{{ dataObject.data[0].row[i] | datetimeFormat }}
</span>
}
@case ('INTEGER') {
<span>
{{ dataObject.data[0].row[i] }}
</span>
}
@case ('DECIMAL') {
<span>
{{ dataObject.data[0].row[i] | formatNumber }}
</span>
}
@case ('CODELOOKUP') {
<span>
{{ datatables.getCodeLookupValue(columnHeader, dataObject.data[0].row[i]) }}
</span>
}
@case ('TEXT') {
<span class="long-text">
{{ dataObject.data[0].row[i] }}
</span>
}
@case ('JSON') {
<textarea
cdkTextareaAutosize="true"
cdkAutosizeMaxRows="20"
cdkAutosizeMinRows="1"
class="json-textarea"
[innerHTML]="dataObject.data[0].row[i].value | prettyPrint"
>
</textarea>
}
@default {
<span>
@if (isValidUrl(dataObject.data[0].row[i])) {
<span class="m-r-5">
<button
mat-icon-button
matTooltip="{{ 'tooltips.View Link' | translate }}"
(click)="openSite(dataObject.data[0].row[i])"
matTooltipPosition="right"
class="small-icon"
>
<fa-icon icon="eye" size="lg"></fa-icon>
</button>
<mat-card class="data-display-card m-t-20">
<mat-card-content>
<div class="data-grid">
@for (columnHeader of dataObject.columnHeaders; track columnHeader; let i = $index) {
<div class="data-item" [ngClass]="setAttributeClass(columnHeader.columnName)">
<div class="data-label mat-body-strong">
{{ datatables.toDisplayLabel(columnHeader.columnName) }}
</div>
<div class="data-value">
@switch (getColumnType(columnHeader.columnDisplayType, columnHeader.columnType)) {
@case ('DATE') {
<span>
{{ dataObject.data[0].row[i] | dateFormat }}
</span>
}
{{ dataObject.data[0].row[i] }}
</span>
}
}
</div>
@case ('DATETIME') {
<span>
{{ dataObject.data[0].row[i] | datetimeFormat }}
</span>
}
@case ('INTEGER') {
<span>
{{ dataObject.data[0].row[i] }}
</span>
}
@case ('DECIMAL') {
<span>
{{ dataObject.data[0].row[i] | formatNumber }}
</span>
}
@case ('CODELOOKUP') {
<span>
{{ datatables.getCodeLookupValue(columnHeader, dataObject.data[0].row[i]) }}
</span>
}
@case ('TEXT') {
<span class="long-text">
{{ dataObject.data[0].row[i] }}
</span>
}
@case ('JSON') {
<textarea
cdkTextareaAutosize="true"
cdkAutosizeMaxRows="20"
cdkAutosizeMinRows="1"
class="json-textarea"
[innerHTML]="dataObject.data[0].row[i].value | prettyPrint"
>
</textarea>
}
@default {
<div class="default-value">
@if (columnHeader.columnName === 'created_at' || columnHeader.columnName === 'updated_at') {
<span>
{{ dataObject.data[0].row[i] | datetimeFormat }}
</span>
} @else if (isValidUrl(dataObject.data[0].row[i])) {
<span class="m-r-5">
<button
mat-icon-button
matTooltip="{{ 'tooltips.View Link' | translate }}"
(click)="openSite(dataObject.data[0].row[i])"
matTooltipPosition="right"
class="small-icon"
>
<fa-icon icon="eye" size="lg"></fa-icon>
</button>
</span>
{{ dataObject.data[0].row[i] }}
} @else {
{{ dataObject.data[0].row[i] }}
}
</div>
}
}
</div>
</div>
}
</div>
}
</div>
</mat-card-content>
</mat-card>
}
</div>
@@ -10,6 +10,67 @@
}
}
.data-display-card {
margin-bottom: 20px;
box-shadow: 0 2px 4px rgb(0 0 0 / 10%);
}
.data-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 100%));
gap: 20px;
padding: 12px 0;
}
.data-item {
display: flex;
flex-direction: column;
padding: 16px;
background-color: rgb(0 0 0 / 2%);
border-radius: 8px;
border-left: 3px solid transparent;
transition: all 0.2s ease;
min-height: 70px;
&:hover {
background-color: rgb(0 0 0 / 4%);
border-left-color: var(--primary-color, #3f51b5);
transform: translateY(-2px);
box-shadow: 0 2px 8px rgb(0 0 0 / 10%);
}
}
.data-label {
font-size: 0.8125rem;
color: rgb(0 0 0 / 54%);
margin-bottom: 8px;
letter-spacing: 0.3px;
font-weight: 500;
line-height: 1.2;
}
.data-value {
font-size: 1.0625rem;
color: rgb(0 0 0 / 87%);
word-break: break-word;
min-height: 28px;
display: flex;
align-items: center;
font-weight: 400;
line-height: 1.5;
span,
div {
width: 100%;
}
}
.default-value {
display: flex;
align-items: center;
width: 100%;
}
.table-name {
padding-left: 2%;
}
@@ -20,11 +81,14 @@
.system-defined {
margin-top: 3px;
background-color: rgb(63 81 181 / 8%);
border-left-color: #3f51b5;
}
.long-text {
word-break: break-all;
word-break: break-word;
white-space: normal;
line-height: 1.5;
}
.small-icon {
@@ -35,4 +99,18 @@
.json-textarea {
width: 100%;
height: 100%;
padding: 12px;
border: 1px solid rgb(0 0 0 / 12%);
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 0.875rem;
background-color: #f5f5f5;
resize: vertical;
}
@media (width <= 768px) {
.data-grid {
grid-template-columns: 100%;
gap: 16px;
}
}
@@ -12,6 +12,7 @@ import { NgClass } from '@angular/common';
import { MatButton, MatIconButton } from '@angular/material/button';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatDivider } from '@angular/material/divider';
import { MatCard, MatCardContent } from '@angular/material/card';
import { CdkTextareaAutosize } from '@angular/cdk/text-field';
import { MatTooltip } from '@angular/material/tooltip';
import { DateFormatPipe } from '../../../../pipes/date-format.pipe';
@@ -28,6 +29,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
MatDivider,
MatCard,
MatCardContent,
NgClass,
CdkTextareaAutosize,
MatIconButton,
@@ -170,6 +173,15 @@ export class DatatableSingleRowComponent implements OnInit {
}
getColumnType(columnDisplayType: string, columnType: string) {
if (
columnType &&
(columnType.toLowerCase().includes('timestamp') ||
columnType.toLowerCase() === 'created_at' ||
columnType.toLowerCase() === 'updated_at')
) {
return 'DATETIME';
}
switch (columnDisplayType) {
case 'DATE': {
return columnDisplayType;