WEB-528 Improve the data visualization in Data Tables (#2942)
This commit is contained in:
@@ -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 {
|
||||
|
||||
+30
-28
@@ -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>
|
||||
|
||||
+53
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -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,
|
||||
|
||||
+78
-68
@@ -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>
|
||||
|
||||
+79
-1
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user