Dataset Profile Editor. Drag and drop with dragula.

This commit is contained in:
Kristian Ntavidi 2021-02-25 14:17:47 +02:00
parent a4db1973d3
commit ddd12cbba8
15 changed files with 254 additions and 77 deletions

View File

@ -31,6 +31,7 @@
"moment": "^2.24.0",
"moment-timezone": "^0.5.26",
"ng-dialog-animation": "^9.0.3",
"ng2-dragula": "^2.1.1",
"ngx-cookie-service": "^2.2.0",
"ngx-cookieconsent": "^2.2.3",
"ngx-dropzone": "^2.2.2",

View File

@ -32,6 +32,7 @@ import { TranslateServerLoader } from './core/services/language/server.loader';
import { MatomoService } from './core/services/matomo/matomo-service';
import { GuidedTourModule } from './library/guided-tour/guided-tour.module';
import { Oauth2DialogModule } from './ui/misc/oauth2-dialog/oauth2-dialog.module';
import { DragulaModule } from 'ng2-dragula';
// AoT requires an exported function for factories
export function HttpLoaderFactory(http: HttpClient, appConfig: ConfigurationService) {
@ -107,7 +108,8 @@ const appearance: MatFormFieldDefaultOptions = {
SidebarModule,
NgcCookieConsentModule.forRoot(cookieConfig),
Oauth2DialogModule,
GuidedTourModule.forRoot()
GuidedTourModule.forRoot(),
DragulaModule.forRoot()
],
declarations: [
AppComponent,

View File

@ -47,6 +47,8 @@ import { DatasetProfileTableOfContents } from './table-of-contents/table-of-cont
import { DatasetProfileTableOfContentsInternalSection } from './table-of-contents/table-of-contents-internal-section/table-of-contents-internal-section';
import { VisibilityRulesService } from '@app/ui/misc/dataset-description-form/visibility-rules/visibility-rules.service';
import {DragDropModule} from '@angular/cdk/drag-drop';
import {DragulaModule} from 'ng2-dragula';
//matrial
import {MatBadgeModule} from '@angular/material/badge';
@ -64,7 +66,8 @@ import { DatasetProfileEditorSectionFieldSetComponent } from './editor/component
DatasetModule,
AngularStickyThingsModule,
DragDropModule,
MatBadgeModule
MatBadgeModule,
DragulaModule
],
declarations: [
DatasetProfileListingComponent,

View File

@ -260,8 +260,8 @@
<li class="list-inline-item">
<mat-icon [matMenuTriggerFor]="inputmenu" >folder</mat-icon>
<li class="list-inline-item" *ngIf="!viewOnly">
<mat-icon [matMenuTriggerFor]="inputmenu" matTooltip="Add new input">folder</mat-icon>
<mat-menu #inputmenu="matMenu">
<!-- <button class="mat-menu-item" (click)="addNewInput(viewTypeEnum.TextArea)">{{enumUtils.toDatasetProfileViewTypeString(viewTypeEnum.TextArea)}}</button>
@ -345,16 +345,16 @@
<!-- <li class="list-inline-item"><mat-icon>folder</mat-icon><small>CommentField</small></li> -->
<li class="list-inline-item">
<mat-checkbox [formControl]="this.form.get('hasCommentField')">Comment Field</mat-checkbox>
<mat-checkbox [formControl]="this.form.get('hasCommentField')" matTooltip="Include comment field">Comment Field</mat-checkbox>
</li>
<li class="list-inline-item">
<mat-checkbox [checked]="isMultiplicityEnabled" (change)="onIsMultiplicityEnabledChange($event)">
<mat-checkbox [checked]="isMultiplicityEnabled" (change)="onIsMultiplicityEnabledChange($event)" matTooltip="Enable multiplicity" [disabled]="viewOnly">
multiplicity
</mat-checkbox>
</li>
<li class="list-inline-item">
<!-- <mat-icon>more_vert</mat-icon> -->
<mat-icon [matMenuTriggerFor]="menu" >more_vert</mat-icon>
<mat-icon [matMenuTriggerFor]="menu" matTooltip="More..">more_vert</mat-icon>
<mat-menu #menu="matMenu">
<!-- TODO to check -->
<mat-checkbox class="mat-menu-item" (click)="$event.stopPropagation()" [(ngModel)]="showDescription">Description</mat-checkbox>

View File

@ -4,16 +4,16 @@
<div class="row justify-content-end mb-1 mt-1">
<div class="col-auto">
<ul class="list-unstyled list-inline d-flex align-items-center">
<li class="list-inline-item">
<mat-slide-toggle [checked]="isRequired" (change)="toggleRequired($event)" labelPosition="before">
<li class="list-inline-item" >
<mat-slide-toggle [checked]="isRequired" (change)="toggleRequired($event)" labelPosition="before" matTooltip="Make input required">
Required
</mat-slide-toggle>
</li>
<li class="list-inline-item">
<mat-icon style="cursor: pointer;" (click)="addNewRule()">visibility</mat-icon>
<li class="list-inline-item" *ngIf="!viewOnly">
<mat-icon style="cursor: pointer;" (click)="addNewRule()" matTooltip="Add visibility rule">visibility</mat-icon>
</li>
<li class="list-inline-item">
<mat-icon style="cursor: pointer;" (click)="onDelete()">delete</mat-icon>
<li class="list-inline-item" *ngIf="!viewOnly">
<mat-icon style="cursor: pointer;" (click)="onDelete()" matTooltip="Delete this input">delete</mat-icon>
</li>
</ul>

View File

@ -1,7 +1,7 @@
<div class="row" id="main-container">
<div class="row" [id]="idprefix+form.get('id').value">
<!-- SECTION INFO -->
<mat-card style="margin-bottom: 2em;" class="col-12">
<mat-card style="margin-bottom: 2em;" class="col-12" >
<mat-card-content>
<app-dataset-profile-editor-section-component
[form]="form"
@ -12,11 +12,10 @@
</mat-card>
<!-- FIELDSET INFO -->
<div class="col-12 drop-list" cdkDropList (cdkDropListDropped)="drop($event)" [cdkDropListAutoScrollDisabled]="false">
<div class="col-12 drop-list" dragula="FIELDSETS" [(dragulaModel)]="form.get('fieldSets').controls">
<div style="margin-bottom: 2em;" class="row"
*ngFor="let fieldset of form.get('fieldSets')?.controls ; let i=index" cdkDrag [id]="fieldset.get('id').value"
[cdkDragDisabled]="!(fieldset.get('id').value === selectedFieldSetId)"
*ngFor="let fieldset of form.get('fieldSets')?.controls ; let i=index"[id]="idprefix+fieldset.get('id').value"
>
<!-- <h4 style="font-weight: bold" class="col-12">
{{'DATASET-PROFILE-EDITOR.STEPS.FORM.SECTION.FIELDS.FIELDS-TITLE' |
@ -35,8 +34,8 @@
[ngClass]="{'field-container-active': fieldset.get('id').value === selectedFieldSetId}">
<mat-card-content>
<mat-card-header *ngIf="fieldset.get('id').value === selectedFieldSetId">
<mat-icon style="display:inline-block; margin: 0px auto; cursor: grab;transform: rotate(90deg);" cdkDragHandle>drag_indicator</mat-icon>
<mat-card-header *ngIf="(fieldset.get('id').value === selectedFieldSetId) && !viewOnly">
<mat-icon class="handle" style="display:inline-block; margin: 0px auto; cursor: grab;transform: rotate(90deg);" cdkDragHandle>drag_indicator</mat-icon>
</mat-card-header>
<app-dataset-profile-editor-composite-field-component [form]="fieldset"
[viewOnly]="viewOnly"

View File

@ -36,8 +36,3 @@ $blue-color-light: #5cf7f2;
.drop-list.cdk-drop-list-dragging {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
#main-container{
// max-height: 100vh;
// overflow: scroll;
}

View File

@ -1,11 +1,14 @@
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
import { Inject } from '@angular/core';
import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
import { FormArray, FormGroup } from '@angular/forms';
import { FieldEditorModel } from '@app/ui/admin/dataset-profile/admin/field-editor-model';
import { FieldSetEditorModel } from '@app/ui/admin/dataset-profile/admin/field-set-editor-model';
import { SectionEditorModel } from '@app/ui/admin/dataset-profile/admin/section-editor-model';
import { BaseComponent } from '@common/base/base.component';
import { Guid } from '@common/types/guid';
import { DragulaService } from 'ng2-dragula';
import { Subscription } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { ToCEntry, ToCEntryType } from '../../../table-of-contents/table-of-contents-entry';
@ -27,8 +30,37 @@ export class DatasetProfileEditorSectionFieldSetComponent extends BaseComponent
@Output() dataNeedsRefresh = new EventEmitter<void> ();
// @Output() fieldsetAdded = new EventEmitter<String>(); //returns the id of the fieldset added
idprefix = "id";
private subs = new Subscription();
private FIELDSETS = 'FIELDSETS';
constructor(
private dragulaService: DragulaService,
private myElement: ElementRef
)
{
super();
if(this.dragulaService.find(this.FIELDSETS)){
this.dragulaService.destroy(this.FIELDSETS);
}
constructor() { super(); }
this.dragulaService.createGroup(this.FIELDSETS,{
moves:(el, container, handle)=>{
if(this.viewOnly) return false;
if(el.id != (this.idprefix+this.tocentry.id)) return false;
if(handle.className.includes('handle')) return true;
return false;
}
});
this.subs.add(this.dragulaService.drop(this.FIELDSETS).subscribe(obs=>{
this.dataNeedsRefresh.emit();
return ;
}));
}
ngOnDestroy(){
this.subs.unsubscribe();
}
ngOnChanges(changes: SimpleChanges): void {
this.initialize();
}
@ -62,6 +94,11 @@ export class DatasetProfileEditorSectionFieldSetComponent extends BaseComponent
this.form = this.tocentry.form;
this.numbering = this.tocentry.numbering;
this._selectedFieldSetId = null;
let el = this.myElement.nativeElement.querySelector("#"+this.idprefix+this.tocentry.id);
if(el){
el.scrollIntoView({behavior: "smooth"});
}
}else if(this.tocentry.type === ToCEntryType.FieldSet){
this.form = this.tocentry.form.parent.parent;
const numberingArray = this.tocentry.numbering.split('.');
@ -75,6 +112,13 @@ export class DatasetProfileEditorSectionFieldSetComponent extends BaseComponent
// const element = document.getElementById(this.selectedFieldSetId);
// element.scrollTop = element.scrollHeight;
// }, 2000);
let el = this.myElement.nativeElement.querySelector("#"+this.idprefix+this.selectedFieldSetId);
// let el = this.myElement.nativeElement.getElementbyId (this.selectedFieldSetId);
if(el){
el.scrollIntoView({behavior: "smooth"});
}
}
}

View File

@ -38,19 +38,45 @@
</div>
<div class="col d-flex justify-content-end">
<button mat-button class="navigate-btn" (click)="stepper.previous()">
Previous
<button mat-button class="navigate-btn" (click)="stepper.previous()" *ngIf="stepper.selectedIndex !=0">
<mat-icon>navigate_before</mat-icon> Previous
</button>
<button mat-button class="navigate-btn ml-2" (click)="stepper.next()">
Next
<button mat-button class="navigate-btn ml-2" (click)="validateStep(stepper.selected);stepper.next();" *ngIf="stepper.selectedIndex != (steps.length-1)">
Next <mat-icon>navigate_next</mat-icon>
</button>
<ng-container *ngIf="stepper.selectedIndex === (steps.length-1)">
<ng-container *ngIf="!viewOnly">
<button mat-button class="navigate-btn ml-2"
(click)='onSubmit()' [disabled]="!form.valid">{{'DATASET-PROFILE-EDITOR.ACTIONS.SAVE' |
translate}}</button>
<button mat-button class="navigate-btn ml-2"
(click)='finalize()' [disabled]="!form.valid">{{'DATASET-PROFILE-EDITOR.ACTIONS.FINALIZE' |
translate}}</button>
</ng-container>
<!-- SAVE BUTTON WHEN FINALIZED-->
<ng-container *ngIf="showUpdateButton()">
<!-- <button mat-button class="navigate-btn ml-2"
(click)='checkFormValidation()'
[disabled]="form.valid">{{'DATASET-PROFILE-EDITOR.ACTIONS.VALIDATE' | translate}}</button> -->
<button mat-button class="navigate-btn ml-2"
(click)='updateFinalized()' [disabled]="!form.valid">{{'DATASET-PROFILE-EDITOR.ACTIONS.UPDATE' |
translate}}</button>
</ng-container>
</ng-container>
</div>
</div>
<mat-horizontal-stepper [linear]="true" #stepper class="stepper" (selectionChange)="onMatStepperSelectionChange($event)">
<mat-step [label]="'DATASET-PROFILE-EDITOR.STEPS.GENERAL-INFO.TITLE' | translate">
<mat-step [label]="'DATASET-PROFILE-EDITOR.STEPS.GENERAL-INFO.TITLE' | translate" [stepControl]="basicInfo">
<!-- <ng-template matStepLabel>{{'DATASET-PROFILE-EDITOR.STEPS.GENERAL-INFO.TITLE' | translate}}
</ng-template> -->
<div class="row">
@ -81,7 +107,7 @@
<div class="col-12">
<!-- <div class="heading">1.3 {{'DMP-EDITOR.FIELDS.LANGUAGE' | translate}}</div> -->
<div class="heading">1.3 {{'DATASET-PROFILE-EDITOR.STEPS.GENERAL-INFO.DATASET-TEMPLATE-LANGUAGE'| translate}}</div>
<div class="heading">1.3 {{'DATASET-PROFILE-EDITOR.STEPS.GENERAL-INFO.DATASET-TEMPLATE-LANGUAGE'| translate}} *</div>
<mat-form-field class="full-width">
<!-- <input matInput formControlName="description" placeholder="{{'DATASET-PROFILE-EDITOR.FIELDS.DATASET-DESCRIPTION' | translate}}" required> -->
<mat-select formControlName="language">
@ -112,7 +138,7 @@
</div>
</div>
</mat-step> -->
<mat-step [label]="'DATASET-PROFILE-EDITOR.STEPS.FORM.TITLE' | translate">
<mat-step [label]="'DATASET-PROFILE-EDITOR.STEPS.FORM.TITLE' | translate" [stepControl]="form">
<!-- <ng-template matStepLabel>{{'DATASET-PROFILE-EDITOR.STEPS.FORM.TITLE' | translate}}</ng-template> -->
<div class="row">
@ -211,7 +237,7 @@
<!-- TOOLBAR -->
<div class="col-auto"
*ngIf="((selectedTocEntry?.type == tocEntryEnumValues.Section)||(selectedTocEntry?.type == tocEntryEnumValues.FieldSet) )&&(selectedTocEntry?.subEntriesType != tocEntryEnumValues.Section)">
*ngIf="((selectedTocEntry?.type == tocEntryEnumValues.Section)||(selectedTocEntry?.type == tocEntryEnumValues.FieldSet) )&&(selectedTocEntry?.subEntriesType != tocEntryEnumValues.Section) && !viewOnly">
<div class="row" class="actions-list bg-white sticky-top" style="top: 2em;">
<mat-list role="list">

View File

@ -534,10 +534,24 @@ export class DatasetProfileEditorComponent extends BaseComponent implements OnIn
}
this.updateOrdinals(this.toCEntries);
return this.toCEntries;
}
private updateOrdinals(tocentries: ToCEntry[]){
if(!tocentries || !tocentries.length) return;
tocentries.forEach((e,idx)=>{
const ordinalControl = e.form.get('ordinal');
if(ordinalControl){
ordinalControl.setValue(idx);
ordinalControl.updateValueAndValidity();
}
this.updateOrdinals(e.subEntries);
});
}
toCEntries:ToCEntry[];
getTocEntries(): ToCEntry[] {
@ -748,7 +762,7 @@ export class DatasetProfileEditorComponent extends BaseComponent implements OnIn
const parentArray = parent.form.get('fieldSets') as FormArray;
const addedFieldSet = parentArray.at(parentArray.length - 1);
// this.selectedTocEntry = this._findTocEntryById(addedFieldSet.get('id').value, this.getTocEntries());
this.selectedTocEntry = this._findTocEntryById(addedFieldSet.get('id').value, this.getTocEntries());
break;
@ -800,6 +814,7 @@ export class DatasetProfileEditorComponent extends BaseComponent implements OnIn
if (pageIndex >= 0) {
//remove page
this._updateSelectedItem(tce);
pages.removeAt(pageIndex);
//clean up sections of removed page
@ -852,6 +867,7 @@ export class DatasetProfileEditorComponent extends BaseComponent implements OnIn
const sections = (this.form.get('sections') as FormArray);
//remove section
this._updateSelectedItem(tce);
sections.removeAt(index);
//update ordinal
@ -872,6 +888,7 @@ export class DatasetProfileEditorComponent extends BaseComponent implements OnIn
}
}
if (index >= 0) {
this._updateSelectedItem(tce);
parentFormArray.removeAt(index);
//update odrinal
@ -901,6 +918,7 @@ export class DatasetProfileEditorComponent extends BaseComponent implements OnIn
}
if(idx>=0){//fieldset found
this._updateSelectedItem(tce);
parentFormArray.removeAt(idx);
//patching order
@ -914,13 +932,38 @@ export class DatasetProfileEditorComponent extends BaseComponent implements OnIn
}
//in case selectedtocentrhy is child of the removed element
this.refreshToCEntries();
}
private _updateSelectedItem(tce: ToCEntry){
if(this.selectedTocEntry ){
if(this.tocEntryIsChildOf(this.selectedTocEntry,tce)){
if(this.selectedTocEntry.type == ToCEntryType.Page){
this.selectedTocEntry = null;
}else{
const parentId = tce.form.parent.parent.get('id').value;
//if first level section
const firstLevelSections = (this.form.get('sections') as FormArray);
let isFirstLevel: boolean = false;
firstLevelSections.controls.forEach(section=>{
if(section.get('id').value === tce.id){
isFirstLevel = true;
}
});
let parentId = null;
if(isFirstLevel){
parentId = tce.form.get('page').value;
}else{
parentId = tce.form.parent.parent.get('id').value
}
// const parentId = tce.form.parent.parent.get('id').value;
if(parentId){
const tocentries = this.getTocEntries();
const parent = this._findTocEntryById(parentId, tocentries);
@ -936,9 +979,6 @@ export class DatasetProfileEditorComponent extends BaseComponent implements OnIn
}
}
}
this.refreshToCEntries();
}
tocEntryIsChildOf(testingChild: ToCEntry,parent: ToCEntry): boolean{
@ -1202,7 +1242,30 @@ export class DatasetProfileEditorComponent extends BaseComponent implements OnIn
return fieldsets;
}
onMatStepperSelectionChange(event: StepperSelectionEvent){
get basicInfo(){
const label = this.form.get('label');
const description = this.form.get('description');
const language = this.form.get('language');
const fg = new FormGroup({
label: label,
description: description,
language: language
})
return fg;
}
onMatStepperSelectionChange(event: StepperSelectionEvent){
if(event.selectedIndex === 2){//preview selected
this.generatePreviewForm();//TODO LAZY LOADING IN THE TEMPLATE
@ -1235,9 +1298,13 @@ export class DatasetProfileEditorComponent extends BaseComponent implements OnIn
const clonedForm = clonedModel.buildForm();
parentArray.push(clonedForm);
//update tocentries and make selected tocentry the cloedn
let entries = this.refreshToCEntries();
this.refreshToCEntries();
const entryfound = this._findTocEntryById(clonedForm.get('id').value, entries);
if(entryfound){
this.selectedTocEntry = entryfound;
}
// //create one field form fieldset
// const field: FieldEditorModel = new FieldEditorModel(); //to ask
@ -1258,5 +1325,11 @@ export class DatasetProfileEditorComponent extends BaseComponent implements OnIn
}
validateStep(step: CdkStep){
if(step.hasError){
this.checkFormValidation();
}
}
}

View File

@ -2,16 +2,16 @@
display: inline-block !important;
}
::ng-deep .status-form .mat-form-field-wrapper {
background-color: white !important;
padding-bottom: 0 !important;
}
// ::ng-deep .status-form .mat-form-field-wrapper {
// background-color: white !important;
// padding-bottom: 0 !important;
// }
::ng-deep .search-form .mat-form-field-wrapper {
background-color: white !important;
padding-bottom: 0 !important;
}
// ::ng-deep .search-form .mat-form-field-wrapper {
// background-color: white !important;
// padding-bottom: 0 !important;
// }
::ng-deep .mat-form-field-appearance-outline .mat-form-field-infix {
padding: 0.3rem 0rem 0.6rem 0rem !important;
}
// ::ng-deep .mat-form-field-appearance-outline .mat-form-field-infix {
// padding: 0.3rem 0rem 0.6rem 0rem !important;
// }

View File

@ -11,7 +11,7 @@ import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'app-dataset-profile-criteria-component',
templateUrl: './dataset-profile.component.html',
styleUrls: ['./dataset-profile.component.scss'],
styleUrls: ['./dataset-profile.component.scss']
})
export class DatasetProfileCriteriaComponent extends BaseCriteriaComponent implements OnInit {

View File

@ -6,7 +6,7 @@
.mat-table {
margin-top: 47px;
margin-bottom: 20px;
// margin-bottom: 20px;
border-radius: 4px;
}
@ -73,32 +73,32 @@ mat-row:hover {
flex-direction: row-reverse !important;
justify-content: space-between !important;
background-color: #f6f6f6;
height: 30px;
min-height: 30px !important;
// height: 30px;
// min-height: 30px !important;
}
::ng-deep .mat-paginator-page-size {
height: 43px;
}
// ::ng-deep .mat-paginator-page-size {
// height: 43px;
// }
::ng-deep .mat-paginator-range-label {
margin: 15px 32px 0 24px !important;
}
// ::ng-deep .mat-paginator-range-label {
// margin: 15px 32px 0 24px !important;
// }
::ng-deep .mat-paginator-range-actions {
width: 55% !important;
min-height: 43px !important;
justify-content: space-between;
}
// ::ng-deep .mat-paginator-range-actions {
// width: 55% !important;
// min-height: 43px !important;
// justify-content: space-between;
// }
::ng-deep .mat-paginator-navigation-previous {
margin-left: auto !important;
}
// ::ng-deep .mat-paginator-navigation-previous {
// margin-left: auto !important;
// }
::ng-deep .mat-icon-button {
height: 30px !important;
font-size: 12px !important;
}
// ::ng-deep .mat-icon-button {
// height: 30px !important;
// font-size: 12px !important;
// }
.import-btn {
background: #ffffff 0% 0% no-repeat padding-box;

View File

@ -55,3 +55,6 @@ import 'zone.js/dist/zone'; // Included with Angular CLI.
* Need to import at least one locale-data with intl.
*/
//import 'intl/locale-data/jsonp/en';
(window as any).global = window;//ng2-dragula

View File

@ -8,6 +8,37 @@
// Guided Tour style
@import '../node_modules/ngx-guided-tour/scss/guided-tour-base-theme.scss';
@import '../node_modules/dragula/dist/dragula.css';
/* in-flight clone */
.gu-mirror {
position: fixed !important;
margin: 0 !important;
z-index: 9999 !important;
opacity: 1;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)";
filter: alpha(opacity=80);
pointer-events: none;
}
/* high-performance display:none; helper */
.gu-hide {
left: -9999px !important;
}
/* added to mirrorContainer (default = body) while dragging */
.gu-unselectable {
-webkit-user-select: none !important;
-moz-user-select: none !important;
-ms-user-select: none !important;
user-select: none !important;
}
/* added to the source element while its mirror is dragged */
.gu-transit {
opacity: 0;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=20)";
filter: alpha(opacity=20);
}
// Custom Theme
// @import "./assets/scss/blue-theme.scss";