Save attributes and base config

This commit is contained in:
Adina Țeudan 2021-04-08 17:44:09 +03:00
parent 8191aa6529
commit 28f052a23d
9 changed files with 402 additions and 90 deletions

View File

@ -4,60 +4,79 @@
<div class="dialog-content">
<div class="sub-header">
<div class="left">
<div class="info">
{{ csvFile.name }}
</div>
<div class="info"><span translate="file-attributes-csv-import.file"> </span> {{ csvFile.name }}</div>
<div class="large-label">
{{ 'file-attributes-csv-import.total-rows' | translate: { rows: parseResult?.data?.length } }}
</div>
</div>
<div class="right">
<div class="red-input-group required w-250">
<mat-form-field floatLabel="always">
<mat-label>{{ 'file-attributes-csv-import.key-column' | translate }}</mat-label>
<mat-select>
<mat-option *ngFor="let field of parseResult?.meta?.fields" [value]="field">
{{ field }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<form [formGroup]="baseConfigForm">
<div class="red-input-group required w-250">
<mat-form-field floatLabel="always">
<mat-label>{{ 'file-attributes-csv-import.key-column' | translate }}</mat-label>
<mat-select formControlName="filenameMappingColumnHeaderName">
<mat-option *ngFor="let field of parseResult?.meta?.fields" [value]="field">
{{ field }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="red-input-group required w-110">
<label translate="file-attributes-csv-import.delimiter"></label>
<input
[(ngModel)]="delimiter"
name="delimiter"
type="text"
[placeholder]="'file-attributes-csv-import.delimiter-placeholder' | translate"
/>
</div>
<div class="red-input-group required w-110">
<label translate="file-attributes-csv-import.delimiter"></label>
<input
formControlName="delimiter"
name="delimiter"
type="text"
[placeholder]="'file-attributes-csv-import.delimiter-placeholder' | translate"
/>
</div>
<div class="red-input-group required w-160">
<label translate="file-attributes-csv-import.encoding"></label>
<input [(ngModel)]="encoding" name="encoding" type="text" [placeholder]="'file-attributes-csv-import.encoding-placeholder' | translate" />
</div>
<div class="red-input-group required w-160">
<label translate="file-attributes-csv-import.encoding"></label>
<input
formControlName="encoding"
name="encoding"
type="text"
[placeholder]="'file-attributes-csv-import.encoding-placeholder' | translate"
/>
</div>
</form>
</div>
</div>
<div class="csv-part">
<div class="left">
<div class="csv-part-header">
<span class="all-caps-label">{{ 'file-attributes-csv-import.available' | translate: { value: parseResult?.meta?.fields.length } }}</span>
<span class="all-caps-label">{{ 'file-attributes-csv-import.selected' | translate: { value: selectedFields.length } }}</span>
<div>
<span class="all-caps-label">{{
'file-attributes-csv-import.available' | translate: { value: parseResult?.meta?.fields.length }
}}</span>
<span class="all-caps-label">{{ 'file-attributes-csv-import.selected' | translate: { value: activeFields.length } }}</span>
</div>
<div class="quick-activation">
<span class="all-caps-label primary pointer" (click)="activateAll()" translate="file-attributes-csv-import.quick-activation.all"></span>
<span
class="all-caps-label primary pointer"
(click)="deactivateAll()"
translate="file-attributes-csv-import.quick-activation.none"
></span>
</div>
</div>
<div class="csv-header-pill-content">
<div
class="csv-header-pill"
*ngFor="let field of parseResult?.meta?.fields"
(mouseenter)="hoveredColumn = field"
*ngFor="let field of parseResult?.fields"
(mouseenter)="hoveredColumn = field.csvColumn"
(mouseleave)="hoveredColumn = undefined"
(click)="toggleFieldActive(field)"
[class.selected]="isActive(field)"
>
<div class="name">
{{ field }}
{{ field.csvColumn }}
</div>
<div class="secondary">
<div class="entry-count small-label">{{ getEntries(field) }} entries</div>
<div class="sample small-label">Sample: {{ getSample(field) }}</div>
<div class="entry-count small-label">{{ getEntries(field.csvColumn) }} entries</div>
<div class="sample small-label">Sample: {{ getSample(field.csvColumn) }}</div>
</div>
</div>
</div>
@ -77,21 +96,116 @@
</ng-container>
</div>
</div>
<div class="right">
<div class="csv-part-header"></div>
<!-- {{ parseResult | json }}-->
<div class="left-container">
<div class="header-item">
<div class="select-all-container">
<div
(click)="toggleSelectAll()"
[class.active]="areAllFieldsSelected"
class="select-oval always-visible"
*ngIf="!areAllFieldsSelected && !areSomeFieldsSelected"
></div>
<mat-icon
*ngIf="areAllFieldsSelected"
(click)="toggleSelectAll()"
class="selection-icon active"
svgIcon="red:radio-selected"
></mat-icon>
<mat-icon
*ngIf="areSomeFieldsSelected && !areAllFieldsSelected"
(click)="toggleSelectAll()"
class="selection-icon"
svgIcon="red:radio-indeterminate"
></mat-icon>
</div>
<span class="all-caps-label">
{{ 'file-attributes-csv-import.table-header.title' | translate: { length: activeFields.length } }}
</span>
</div>
<div class="table-header" redactionSyncWidth="table-item">
<div class="select-oval-placeholder"></div>
<redaction-table-col-name label="file-attributes-csv-import.table-col-names.name"></redaction-table-col-name>
<redaction-table-col-name label="file-attributes-csv-import.table-col-names.type"></redaction-table-col-name>
<redaction-table-col-name
label="file-attributes-csv-import.table-col-names.read-only"
class="flex-center"
icon="red:read-only"
></redaction-table-col-name>
<redaction-table-col-name
label="file-attributes-csv-import.table-col-names.display"
class="flex-center"
icon="red:visibility"
></redaction-table-col-name>
<div></div>
<div class="scrollbar-placeholder"></div>
</div>
<cdk-virtual-scroll-viewport [itemSize]="50" redactionHasScrollbar>
<!-- Table lines -->
<div
class="table-item"
*cdkVirtualFor="let field of activeFields"
(mouseenter)="hoveredColumn = field.csvColumn"
(mouseleave)="hoveredColumn = undefined"
>
<div class="pr-0" (click)="toggleFieldSelected(field.csvColumn)">
<div *ngIf="!isFieldSelected(field.csvColumn)" class="select-oval"></div>
<mat-icon class="selection-icon active" *ngIf="isFieldSelected(field.csvColumn)" svgIcon="red:radio-selected"></mat-icon>
</div>
<div>{{ field.name }}</div>
<div>
<div class="red-input-group">
<mat-form-field class="no-label">
<mat-select [(ngModel)]="field.type">
<mat-option *ngFor="let type of ['Text', 'Number', 'Date']" [value]="type">
{{ 'file-attributes-csv-import.types.' + type | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="center">
<mat-slide-toggle [(ngModel)]="field.readonly" color="primary"></mat-slide-toggle>
</div>
<div class="center"><mat-slide-toggle [(ngModel)]="field.display" color="primary"></mat-slide-toggle></div>
<div class="actions-container">
<div class="action-buttons">
<!-- <redaction-circle-button-->
<!-- (action)="openAddEditUserDialog($event, field)"-->
<!-- tooltip="user-listing.action.edit"-->
<!-- type="dark-bg"-->
<!-- icon="red:edit"-->
<!-- >-->
<!-- </redaction-circle-button>-->
<!-- <redaction-circle-button-->
<!-- (action)="openDeleteUserDialog([field], $event)"-->
<!-- tooltip="user-listing.action.delete"-->
<!-- type="dark-bg"-->
<!-- icon="red:trash"-->
<!-- [disabled]="field.userId === userService.userId"-->
<!-- >-->
<!-- </redaction-circle-button>-->
</div>
</div>
<div class="scrollbar-placeholder"></div>
</div>
</cdk-virtual-scroll-viewport>
</div>
</div>
</div>
<div class="dialog-actions">
<button color="primary" mat-flat-button>
<button color="primary" mat-flat-button (click)="save()" [disabled]="baseConfigForm.invalid">
{{ 'file-attributes-csv-import.save' | translate }}
</button>
<button mat-flat-button>
{{ 'file-attributes-csv-import.cancel' | translate }}
</button>
<div class="all-caps-label cancel">{{ 'file-attributes-csv-import.cancel' | translate }}</div>
</div>
<redaction-circle-button icon="red:close" mat-dialog-close class="dialog-close"></redaction-circle-button>

View File

@ -7,53 +7,67 @@
margin-bottom: 25px;
.left {
width: 375px;
display: flex;
justify-content: center;
flex-direction: column;
padding-left: 32px;
min-width: 376px;
box-sizing: border-box;
border-right: 1px solid $separator;
.info {
margin-bottom: 8px;
> span {
font-weight: 500;
}
}
.large-label {
font-weight: bold;
}
}
.right {
.right > form {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding-left: 16px;
.red-input-group {
margin-left: 8px;
margin-right: 8px;
height: 60px;
margin: 0 8px;
}
}
}
.dialog-content {
padding-bottom: 0;
padding: 24px 0 0;
}
.csv-part {
margin-left: -32px;
margin-right: -32px;
display: flex;
max-height: calc(90vh - 251px);
.csv-part-header {
height: 50px;
box-sizing: border-box;
background: $white;
border-top: 1px solid $grey-4;
border-bottom: 1px solid $grey-4;
border-top: 1px solid $separator;
border-bottom: 1px solid $separator;
display: flex;
align-items: center;
padding-left: 16px;
padding-right: 16px;
justify-content: space-between;
padding: 0 16px;
> *:not(:last-child)::after {
> :not(.quick-activation) > *:not(:last-child)::after {
content: '-';
margin: 0 4px;
}
.quick-activation > :not(:last-child) {
margin-right: 10px;
}
}
.csv-part-content {
@ -71,11 +85,12 @@
text-align: center;
color: $grey-7;
line-height: 15px;
font-weight: 500;
}
> *:not(.no-hovered-column) {
height: 30px;
border-bottom: 1px solid $grey-4;
border-bottom: 1px solid $separator;
display: flex;
padding: 0 16px;
align-items: center;
@ -84,7 +99,7 @@
}
}
.left {
> .left {
width: 375px;
background: $grey-2;
@ -92,47 +107,92 @@
overflow: auto;
padding: 4px 10px;
background: $grey-2;
height: calc(100% - 60px);
height: calc(100% - 58px);
@include no-scroll-bar;
.csv-header-pill {
height: 32px;
min-height: 32px;
margin: 6px auto;
border-radius: 8px;
padding: 10px;
background: $white;
cursor: pointer;
display: flex;
flex-direction: column;
.name {
}
.secondary {
display: flex;
justify-content: space-between;
margin-top: 2px;
.entry-count {
white-space: nowrap;
margin-right: 10px;
}
.sample {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@include line-clamp(1);
}
}
&.selected {
background-color: $primary;
color: $white;
}
}
}
border-right: 1px solid $grey-4;
border-right: 1px solid $separator;
}
.center {
width: 149px;
> .center {
width: 150px;
min-width: 150px;
background: $grey-2;
border-right: 1px solid $grey-4;
border-right: 1px solid $separator;
}
.right {
overflow: auto;
max-width: calc(90vw - 525px);
flex: 1 1 auto;
> .left-container {
width: 100%;
redaction-table-col-name::ng-deep {
> div {
padding: 0 13px 0 10px !important;
}
}
.header-item {
padding: 0 24px 0 10px;
box-shadow: none;
border-top: 1px solid $separator;
}
cdk-virtual-scroll-viewport {
height: calc(100% - 80px);
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: auto 2fr 150px auto auto auto 11px;
.table-item {
> div {
height: 50px;
&:not(.scrollbar-placeholder) {
padding-left: 10px;
&.center {
align-items: center;
}
}
}
}
}
&.has-scrollbar:hover {
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: auto 2fr 150px auto auto auto;
}
}
}
}
}

View File

@ -1,8 +1,23 @@
import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AppStateService } from '../../../../state/app-state.service';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import * as Papa from 'papaparse';
import { FileAttributeConfig, FileAttributesConfig, FileAttributesControllerService } from '@redaction/red-ui-http';
enum FieldType {
Text = 'Text',
Number = 'Number',
Date = 'Date'
}
interface Field {
csvColumn: string;
name: string;
type: FieldType;
readonly: boolean;
display: boolean;
}
@Component({
selector: 'redaction-file-attributes-csv-import-dialog',
@ -12,22 +27,28 @@ import * as Papa from 'papaparse';
export class FileAttributesCsvImportDialogComponent implements OnInit {
public csvFile: File;
public ruleSetId: string;
public parseResult: any;
public encoding = 'UTF-8';
public delimiter: string = undefined;
public parseResult: { data: any[]; errors: any[]; meta: any; fields: Field[] };
public hoveredColumn: string;
selectedFields: any[] = [];
public activeFields: Field[] = [];
public selectedFields: string[] = [];
public baseConfigForm: FormGroup;
constructor(
private readonly _appStateService: AppStateService,
private readonly _formBuilder: FormBuilder,
private readonly _fileAttributesControllerService: FileAttributesControllerService,
public dialogRef: MatDialogRef<FileAttributesCsvImportDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: { csv: File; ruleSetId: string }
) {
this.csvFile = data.csv;
this.ruleSetId = data.ruleSetId;
this.baseConfigForm = this._formBuilder.group({
filenameMappingColumnHeaderName: [undefined, Validators.required],
delimiter: [undefined, Validators.required],
encoding: ['UTF-8', Validators.required]
});
this._readFile();
}
@ -37,24 +58,25 @@ export class FileAttributesCsvImportDialogComponent implements OnInit {
const reader = new FileReader();
reader.addEventListener('load', async (event) => {
const parsedCsv = <any>event.target.result;
this.parseResult = Papa.parse(parsedCsv, { header: true, delimiter: this.delimiter });
if (!this.delimiter) {
this.delimiter = this.parseResult.meta.delimiter;
this.parseResult = Papa.parse(parsedCsv, { header: true, delimiter: this.baseConfigForm.get('delimiter').value });
if (!this.baseConfigForm.get('delimiter').value) {
this.baseConfigForm.patchValue({ delimiter: this.parseResult.meta.delimiter });
}
this.parseResult.fields = this.parseResult.meta.fields.map((field) => this._buildField(field));
console.log(this.parseResult);
});
reader.readAsText(this.csvFile, this.encoding);
reader.readAsText(this.csvFile, this.baseConfigForm.get('encoding').value);
}
getSample(field: string) {
return this.parseResult?.data ? this.parseResult?.data[0][field] : '';
public getSample(csvColumn: string) {
return this.parseResult?.data ? this.parseResult?.data[0][csvColumn] : '';
}
getEntries(field: any) {
public getEntries(csvColumn: string) {
if (this.parseResult?.data) {
let count = 0;
for (const entry of this.parseResult.data) {
if (entry[field]) {
if (entry[csvColumn]) {
count++;
}
}
@ -63,4 +85,92 @@ export class FileAttributesCsvImportDialogComponent implements OnInit {
return 0;
}
}
public isActive(field: Field): boolean {
return this.activeFields.indexOf(field) !== -1;
}
public toggleFieldActive(field: Field) {
if (!this.isActive(field)) {
this.activeFields = [...this.activeFields, field];
} else {
this.activeFields.splice(this.activeFields.indexOf(field), 1);
this.activeFields = [...this.activeFields];
if (this.isFieldSelected(field.csvColumn)) {
this.toggleFieldSelected(field.csvColumn);
}
}
}
private _buildField(csvColumn: string): Field {
return {
csvColumn,
name: csvColumn,
type: FieldType.Text,
readonly: false,
display: true
};
}
public activateAll() {
this.activeFields = [...this.parseResult.fields];
}
public deactivateAll() {
this.activeFields = [];
this.selectedFields = [];
}
public toggleFieldSelected(field: string) {
const idx = this.selectedFields.indexOf(field);
if (idx === -1) {
this.selectedFields.push(field);
} else {
this.selectedFields.splice(idx, 1);
}
}
public toggleSelectAll() {
if (this.areSomeFieldsSelected) {
this.selectedFields = [];
} else {
this.selectedFields = this.activeFields.map((field) => field.csvColumn);
}
}
public get areAllFieldsSelected() {
return this.activeFields.length !== 0 && this.selectedFields.length === this.activeFields.length;
}
public get areSomeFieldsSelected() {
return this.selectedFields.length > 0;
}
public isFieldSelected(field: string) {
return this.selectedFields.indexOf(field) !== -1;
}
public async save() {
const promises: Promise<FileAttributeConfig | FileAttributesConfig>[] = [
this._fileAttributesControllerService.addOrUpdateFileAttributesBaseConfig(this.baseConfigForm.getRawValue(), this.ruleSetId).toPromise()
];
for (const field of this.activeFields) {
promises.push(
this._fileAttributesControllerService
.setFileAttributesConfiguration(
{
csvColumnHeader: field.csvColumn,
editable: !field.readonly,
label: field.name,
visible: field.display
},
this.ruleSetId
)
.toPromise()
);
}
await Promise.all(promises);
this.dialogRef.close(true);
}
}

View File

@ -127,8 +127,7 @@ export class AdminDialogService {
public openImportFileAttributeCSVDialog(csv: File, ruleSetId: string, cb?: Function): MatDialogRef<FileAttributesCsvImportDialogComponent> {
const ref = this._dialog.open(FileAttributesCsvImportDialogComponent, {
...largeDialogConfig,
data: { csv, ruleSetId },
autoFocus: true
data: { csv, ruleSetId }
});
ref.afterClosed().subscribe((result) => {

View File

@ -1,4 +1,5 @@
<div (click)="withSort && toggleSort.emit(column)" [class.pointer]="withSort" [ngClass]="class">
<mat-icon *ngIf="!!icon" [svgIcon]="icon"></mat-icon>
<span [translate]="label" class="all-caps-label"></span>
<div class="sort-arrows-container" *ngIf="withSort" [class.force-display]="activeSortingOption.column === column">
<mat-icon *ngIf="activeSortingOption?.order === 'asc'" svgIcon="red:sort-asc"></mat-icon>

View File

@ -11,6 +11,13 @@
width: 100%;
line-height: 11px;
padding: 0 24px;
> mat-icon {
width: 10px;
height: 10px;
margin-right: 6px;
opacity: 0.7;
}
}
.flex-end {

View File

@ -12,6 +12,7 @@ export class TableColNameComponent implements OnInit {
@Input() public label: string;
@Input() public withSort = false;
@Input() public class: string;
@Input() public icon: string;
@Output() public toggleSort = new EventEmitter<string>();
constructor() {}

View File

@ -1120,6 +1120,25 @@
"available": "{{value}} available",
"selected": "{{value}} selected",
"csv-column": "CSV Column",
"no-hovered-column": "Preview CSV column by hovering the entry."
"no-hovered-column": "Preview CSV column by hovering the entry.",
"table-header": {
"title": "{{length}} file attributes"
},
"file": "File:",
"table-col-names": {
"name": "Name",
"type": "Type",
"read-only": "Read-Only",
"display": "Display"
},
"quick-activation": {
"all": "All",
"none": "None"
},
"types": {
"Text": "Free Text",
"Number": "Number",
"Date": "Date"
}
}
}

View File

@ -55,7 +55,8 @@ form {
display: none;
}
.mat-form-field-wrapper {
.mat-form-field-wrapper,
.mat-form-field-infix {
padding-bottom: 0;
}