added annotation mentions
This commit is contained in:
parent
a172defde8
commit
d315500af8
|
@ -40,6 +40,7 @@
|
||||||
"ngx-dropzone": "^3.0.0",
|
"ngx-dropzone": "^3.0.0",
|
||||||
"ngx-guided-tour": "^2.0.1",
|
"ngx-guided-tour": "^2.0.1",
|
||||||
"ngx-matomo-client": "^6.2.0",
|
"ngx-matomo-client": "^6.2.0",
|
||||||
|
"ngx-mentions": "^16.2.0",
|
||||||
"rxjs": "^7.4.0",
|
"rxjs": "^7.4.0",
|
||||||
"tinymce": "^7.2.0",
|
"tinymce": "^7.2.0",
|
||||||
"ts-simple-nameof": "^1.3.1",
|
"ts-simple-nameof": "^1.3.1",
|
||||||
|
@ -9838,6 +9839,18 @@
|
||||||
"@angular/core": "^17.0.0 || ^18.0.0"
|
"@angular/core": "^17.0.0 || ^18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ngx-mentions": {
|
||||||
|
"version": "16.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ngx-mentions/-/ngx-mentions-16.2.0.tgz",
|
||||||
|
"integrity": "sha512-D/lsJmlnb0Ifjd2GnSSHy5/GGfYhw6Xmzip22Z1pYipfCDSfgU00UfhsxHLZyiwo0cljj2vRwg5OlAH4PHP1Fg==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/common": ">=16.0.0",
|
||||||
|
"@angular/core": ">=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nice-napi": {
|
"node_modules/nice-napi": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz",
|
||||||
|
|
|
@ -44,6 +44,7 @@
|
||||||
"ngx-dropzone": "^3.0.0",
|
"ngx-dropzone": "^3.0.0",
|
||||||
"ngx-guided-tour": "^2.0.1",
|
"ngx-guided-tour": "^2.0.1",
|
||||||
"ngx-matomo-client": "^6.2.0",
|
"ngx-matomo-client": "^6.2.0",
|
||||||
|
"ngx-mentions": "^16.2.0",
|
||||||
"rxjs": "^7.4.0",
|
"rxjs": "^7.4.0",
|
||||||
"tinymce": "^7.2.0",
|
"tinymce": "^7.2.0",
|
||||||
"ts-simple-nameof": "^1.3.1",
|
"ts-simple-nameof": "^1.3.1",
|
||||||
|
|
|
@ -27,12 +27,7 @@
|
||||||
<form [formGroup]="threadFormGroup">
|
<form [formGroup]="threadFormGroup">
|
||||||
<div class="row mt-2 mb-3">
|
<div class="row mt-2 mb-3">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<text-area-with-mentions [form]="threadFormGroup.get('text')" [planUsers]="planUsers"></text-area-with-mentions>
|
||||||
<mat-label>{{'ANNOTATION-DIALOG.THREADS.NEW-THREAD' | translate}}</mat-label>
|
|
||||||
<textarea matInput autocomplete="off" formControlName="text" required appTextareaAutoresize></textarea>
|
|
||||||
<mat-error *ngIf="threadFormGroup.get('text').hasError('backendError')">{{threadFormGroup.get('text').getError('backendError')?.message}}</mat-error>
|
|
||||||
<mat-error *ngIf="threadFormGroup.get('text').hasError('required')">{{'COMMONS.VALIDATION.REQUIRED' | translate}}</mat-error>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<mat-button-toggle-group appearance="standard" name="fontStyle" hideSingleSelectionIndicator="true" [formControl]="threadFormGroup.get('protectionType')" aria-label="Font Style" required>
|
<mat-button-toggle-group appearance="standard" name="fontStyle" hideSingleSelectionIndicator="true" [formControl]="threadFormGroup.get('protectionType')" aria-label="Font Style" required>
|
||||||
|
@ -96,7 +91,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mt-3">
|
<div class="row mt-3">
|
||||||
<div class="col-12 annotation-full-text">{{getParentAnnotation(thread).payload}}</div>
|
<ng-container *ngFor="let annotationItem of getParentAnnotation(thread).payload">
|
||||||
|
<div *ngIf="!annotationItem.isMention; else mention">{{annotationItem.payload}} </div>
|
||||||
|
<ng-template #mention><div><span class="highlight-user-mention">{{annotationItem.payload}}</span> </div></ng-template>
|
||||||
|
</ng-container>
|
||||||
<div class="col-12 align-self-end">
|
<div class="col-12 align-self-end">
|
||||||
<div class="row mt-1">
|
<div class="row mt-1">
|
||||||
<button mat-button (click)="enableReply(thread)" class="action-button" [ngClass]="this.replyEnabledPerThread[thread] ? 'active-action' : ''" [disabled]="this.replyEnabledPerThread[thread]">{{ 'ANNOTATION-DIALOG.THREADS.REPLY' | translate}}</button>
|
<button mat-button (click)="enableReply(thread)" class="action-button" [ngClass]="this.replyEnabledPerThread[thread] ? 'active-action' : ''" [disabled]="this.replyEnabledPerThread[thread]">{{ 'ANNOTATION-DIALOG.THREADS.REPLY' | translate}}</button>
|
||||||
|
@ -139,7 +137,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mt-3 pt-1 pb-1">
|
<div class="row mt-3 pt-1 pb-1">
|
||||||
<div class="col-12 annotation-full-text">{{annotation.payload}}</div>
|
<div class="col-12 annotation-full-text" [innerHtml]="parsePayload(annotation.payload)"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -154,10 +152,7 @@
|
||||||
<div class="col-12 mt-2">
|
<div class="col-12 mt-2">
|
||||||
<div class="row new-reply mr-0">
|
<div class="row new-reply mr-0">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<text-area-with-mentions [form]="this.threadReplyTextsFG[thread.toString()].get('replyText')" [planUsers]="planUsers"></text-area-with-mentions>
|
||||||
<mat-label>{{'ANNOTATION-DIALOG.THREADS.REPLY' | translate}}</mat-label>
|
|
||||||
<textarea matInput autocomplete="off" [formControl]="this.threadReplyTextsFG[thread.toString()].get('replyText')" required appTextareaAutoresize></textarea>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto p-0 send-msg">
|
<div class="col-auto p-0 send-msg">
|
||||||
<button mat-icon-button (click)="replyThread(thread)" matTooltip="{{'ANNOTATION-DIALOG.THREADS.REPLY' | translate}}">
|
<button mat-icon-button (click)="replyThread(thread)" matTooltip="{{'ANNOTATION-DIALOG.THREADS.REPLY' | translate}}">
|
||||||
|
|
|
@ -146,4 +146,14 @@ $mat-card-header-size: 40px !default;
|
||||||
|
|
||||||
::ng-deep .statuses-menu.mat-mdc-menu-panel {
|
::ng-deep .statuses-menu.mat-mdc-menu-panel {
|
||||||
min-width: initial !important;
|
min-width: initial !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::ng-deep .highlight-user-mention {
|
||||||
|
padding: 1px 2px;
|
||||||
|
margin: -1px -2px;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
background-color: #e1f2fe;
|
||||||
|
color: #3f5163;
|
||||||
|
opacity: .6;
|
||||||
|
}
|
||||||
|
|
|
@ -24,6 +24,14 @@ import { AnnotationStatusArrayEditorModel } from './annotation-status-editor.mod
|
||||||
import { StatusService } from '@annotation-service/services/http/status.service';
|
import { StatusService } from '@annotation-service/services/http/status.service';
|
||||||
import { ConfigurationService } from '@app/core/services/configuration/configuration.service';
|
import { ConfigurationService } from '@app/core/services/configuration/configuration.service';
|
||||||
import { MatSelectionList } from '@angular/material/list';
|
import { MatSelectionList } from '@angular/material/list';
|
||||||
|
import { PlanUser } from '@app/core/model/plan/plan';
|
||||||
|
import { DomSanitizer } from '@angular/platform-browser';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
|
interface AnnotationPayloadItem {
|
||||||
|
isMention: boolean;
|
||||||
|
payload: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-annotation-dialog',
|
selector: 'app-annotation-dialog',
|
||||||
|
@ -53,10 +61,9 @@ export class AnnotationDialogComponent extends BaseComponent {
|
||||||
private formBuilder: FormBuilder = new FormBuilder();
|
private formBuilder: FormBuilder = new FormBuilder();
|
||||||
public annotationStatusFormGroup: UntypedFormGroup;
|
public annotationStatusFormGroup: UntypedFormGroup;
|
||||||
public listingStatuses: Status[] = [];
|
public listingStatuses: Status[] = [];
|
||||||
public planUsersMentionNames: string[] = [];
|
public planUsers: PlanUser[] = [];
|
||||||
|
|
||||||
@ViewChild('annotationStatus') annotationStatus: MatSelectionList;
|
|
||||||
|
|
||||||
|
@ViewChild('annotationStatus') annotationStatus: MatSelectionList;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public dialogRef: MatDialogRef<AnnotationDialogComponent>,
|
public dialogRef: MatDialogRef<AnnotationDialogComponent>,
|
||||||
|
@ -71,12 +78,13 @@ export class AnnotationDialogComponent extends BaseComponent {
|
||||||
private statusService: StatusService,
|
private statusService: StatusService,
|
||||||
protected routerUtils: RouterUtilsService,
|
protected routerUtils: RouterUtilsService,
|
||||||
private configurationService: ConfigurationService,
|
private configurationService: ConfigurationService,
|
||||||
|
private sanitizer: DomSanitizer,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
this.entityId = data.entityId;
|
this.entityId = data.entityId;
|
||||||
this.anchor = data.anchor;
|
this.anchor = data.anchor;
|
||||||
this.entityType = data.entityType;
|
this.entityType = data.entityType;
|
||||||
this.planUsersMentionNames = data.planUsers.map(x => x.user.name);
|
this.planUsers = data.planUsers;
|
||||||
dialogRef.backdropClick().pipe(takeUntil(this._destroyed)).subscribe(() => dialogRef.close(this.changesMade));
|
dialogRef.backdropClick().pipe(takeUntil(this._destroyed)).subscribe(() => dialogRef.close(this.changesMade));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,7 +187,11 @@ export class AnnotationDialogComponent extends BaseComponent {
|
||||||
this.comments.forEach(element => {
|
this.comments.forEach(element => {
|
||||||
this.threadReplyTextsFG[element.threadId.toString()] = this.formBuilder.group({ replyText: new FormControl(null, [Validators.required]) });
|
this.threadReplyTextsFG[element.threadId.toString()] = this.formBuilder.group({ replyText: new FormControl(null, [Validators.required]) });
|
||||||
this.annotationsPerThread[element.threadId.toString()] = data.items.filter(x => x.threadId === element.threadId && x.id !== element.id).sort((a1, a2) => new Date(a1.timeStamp).getTime() - new Date(a2.timeStamp).getTime());
|
this.annotationsPerThread[element.threadId.toString()] = data.items.filter(x => x.threadId === element.threadId && x.id !== element.id).sort((a1, a2) => new Date(a1.timeStamp).getTime() - new Date(a2.timeStamp).getTime());
|
||||||
this.parentAnnotationsPerThread[element.threadId.toString()] = data.items.filter(x => x.threadId === element.threadId && x.id === element.id)[0];
|
|
||||||
|
let parentAnnotation: any = data.items.filter(x => x.threadId === element.threadId && x.id === element.id)[0];
|
||||||
|
parentAnnotation.payload = this.parsePayload(parentAnnotation.payload);
|
||||||
|
this.parentAnnotationsPerThread[element.threadId.toString()] = parentAnnotation;
|
||||||
|
|
||||||
this.threads.add(element.threadId);
|
this.threads.add(element.threadId);
|
||||||
});
|
});
|
||||||
// create annotation status array to handle each annotation
|
// create annotation status array to handle each annotation
|
||||||
|
@ -340,11 +352,52 @@ export class AnnotationDialogComponent extends BaseComponent {
|
||||||
return control.valid;
|
return control.valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parsePayload(payload: string): AnnotationPayloadItem[] {
|
||||||
|
|
||||||
|
if (!this.planUsers) return [{ isMention: false, payload: payload}];
|
||||||
|
if (this.planUsers.length == 0) [{ isMention: false, payload: payload}];
|
||||||
|
|
||||||
|
let payloadItems: AnnotationPayloadItem[] = [];
|
||||||
|
|
||||||
|
const mentionRegExp = new RegExp(/\@\{\{userid:[a-zA-Z0-9\-]*\}\}/g);
|
||||||
|
payloadItems = payload.split(/(?=\@\{\{userid:[a-zA-Z0-9\-]*\}\})|(?<=\@\{\{userid:[a-zA-Z0-9\-]*\}\})/g)
|
||||||
|
.filter( p => p!=null && p!='')
|
||||||
|
.map((p)=> {
|
||||||
|
let annotationItem: AnnotationPayloadItem = { isMention: false, payload: p};
|
||||||
|
|
||||||
|
if (mentionRegExp.exec(p)) {
|
||||||
|
annotationItem.isMention = true;
|
||||||
|
annotationItem.payload = this.planUsers.find(u => p.includes(u.id.toString()))?.user?.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return annotationItem;
|
||||||
|
});
|
||||||
|
|
||||||
|
return payloadItems;
|
||||||
|
|
||||||
|
// for (let planUser of this.planUsers) {
|
||||||
|
// payloadItems = payload.split(`@{{userid:${planUser.id}}}`).map({isMention }).join('<span class="highlight-user-mention">'+planUser?.user?.name+'</span>');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return this.sanitizer.bypassSecurityTrustHtml(payload);
|
||||||
|
}
|
||||||
|
// parsePayload(payload: string): AnnotationPayloadItem[] {
|
||||||
|
|
||||||
|
// if (!this.planUsers) return [{ isMention: false, payload: payload}];
|
||||||
|
// if (this.planUsers.length == 0) [{ isMention: false, payload: payload}];
|
||||||
|
|
||||||
|
// let payloadItems = [];
|
||||||
|
// // let payloadItems = [];
|
||||||
|
|
||||||
|
// for (let planUser of this.planUsers) {
|
||||||
|
// payloadItems = payload.split(`@{{userid:${planUser.id}}}`).map({isMention }).join('<span class="highlight-user-mention">'+planUser?.user?.name+'</span>');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return this.sanitizer.bypassSecurityTrustHtml(payload);
|
||||||
|
// }
|
||||||
|
|
||||||
private onCallbackAnnotationStatusSuccess() {
|
private onCallbackAnnotationStatusSuccess() {
|
||||||
this.uiNotificationService.snackBarNotification(this.language.instant('ANNOTATION-DIALOG.ANNOTATION-STATUS.SUCCESS'), SnackBarNotificationLevel.Success);
|
this.uiNotificationService.snackBarNotification(this.language.instant('ANNOTATION-DIALOG.ANNOTATION-STATUS.SUCCESS'), SnackBarNotificationLevel.Success);
|
||||||
this.refreshAnnotations();
|
this.refreshAnnotations();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { CommonFormsModule } from '@common/forms/common-forms.module';
|
||||||
import { CommonUiModule } from '@common/ui/common-ui.module';
|
import { CommonUiModule } from '@common/ui/common-ui.module';
|
||||||
import { AnnotationDialogComponent } from './annotation-dialog.component';
|
import { AnnotationDialogComponent } from './annotation-dialog.component';
|
||||||
import { AutoCompleteModule } from '@app/library/auto-complete/auto-complete.module';
|
import { AutoCompleteModule } from '@app/library/auto-complete/auto-complete.module';
|
||||||
|
import { TextAreaWithMentionsComponent } from '../components/text-area-with-mentions.component';
|
||||||
|
import { NgxMentionsModule } from 'ngx-mentions';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -11,12 +13,14 @@ import { AutoCompleteModule } from '@app/library/auto-complete/auto-complete.mod
|
||||||
CommonFormsModule,
|
CommonFormsModule,
|
||||||
FormattingModule,
|
FormattingModule,
|
||||||
AutoCompleteModule,
|
AutoCompleteModule,
|
||||||
|
NgxMentionsModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
AnnotationDialogComponent,
|
AnnotationDialogComponent,
|
||||||
|
TextAreaWithMentionsComponent,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
AnnotationDialogComponent,
|
AnnotationDialogComponent,
|
||||||
]
|
],
|
||||||
})
|
})
|
||||||
export class AnnotationDialogModule { }
|
export class AnnotationDialogModule { }
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
<div class="relative-block-container w-100">
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label>{{'ANNOTATION-DIALOG.THREADS.NEW-THREAD' | translate}}</mat-label>
|
||||||
|
<textarea
|
||||||
|
[formControl]="internalForm"
|
||||||
|
placeholder="Enter '@' or '#' and start typing..."
|
||||||
|
class="w-100"
|
||||||
|
rows="2"
|
||||||
|
matInput
|
||||||
|
appTextareaAutoresize
|
||||||
|
#textareaRef
|
||||||
|
></textarea>
|
||||||
|
<ngx-mentions [textInputElement]="textareaRef" style="position: absolute; left: 0;" class="w-100"
|
||||||
|
[menuTemplate]="menuTemplate"
|
||||||
|
[mentionsConfig]="mentionsConfig"
|
||||||
|
[searchRegexp]="searchRegexp"
|
||||||
|
[mentions]="mentions"
|
||||||
|
[removeWholeTagOnBackspace]="true"
|
||||||
|
(search)="loadChoices($event)"
|
||||||
|
(selectedChoicesChange)="onSelectedChoicesChange($event)"
|
||||||
|
(menuShow)="onMenuShow()"
|
||||||
|
(menuHide)="onMenuHide()"
|
||||||
|
></ngx-mentions>
|
||||||
|
<mat-error *ngIf="form.hasError('backendError')">{{form.getError('backendError')?.message}}</mat-error>
|
||||||
|
<mat-error *ngIf="form.hasError('required')">{{'COMMONS.VALIDATION.REQUIRED' | translate}}</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<ng-template #menuTemplate
|
||||||
|
let-selectChoice="selectChoice">
|
||||||
|
<ngx-text-input-autocomplete-menu *ngIf="choices" [choices]="choices"
|
||||||
|
[getDisplayLabel]="getDisplayLabel"
|
||||||
|
(selectChoice)="selectChoice($event)">
|
||||||
|
</ngx-text-input-autocomplete-menu>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
|
@ -0,0 +1,29 @@
|
||||||
|
.relative-block-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relative-block-container div{
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relative-block-container textarea {
|
||||||
|
line-height: 1.4;
|
||||||
|
word-spacing:2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formatted-text {
|
||||||
|
margin: 3rem 0;
|
||||||
|
width: 300px;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep.ngx-selectable-list {
|
||||||
|
max-height: 150px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep.ngx-text-highlight-tag {
|
||||||
|
letter-spacing: -0.01rem !important;
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { Component, Input, OnDestroy, OnInit } from "@angular/core";
|
||||||
|
import { FormBuilder, FormControl, Validators } from "@angular/forms";
|
||||||
|
import { PlanUser } from "@app/core/model/plan/plan";
|
||||||
|
import { ChoiceWithIndices } from "ngx-mentions";
|
||||||
|
import { Observable, Subscription } from "rxjs";
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'text-area-with-mentions',
|
||||||
|
templateUrl: './text-area-with-mentions.component.html',
|
||||||
|
styleUrls: ['./text-area-with-mentions.component.scss'],
|
||||||
|
})
|
||||||
|
export class TextAreaWithMentionsComponent implements OnInit, OnDestroy {
|
||||||
|
USER_TRIGGER_CHARACTER: string = '@'
|
||||||
|
|
||||||
|
@Input() form!: FormControl;
|
||||||
|
@Input() planUsers: PlanUser[];
|
||||||
|
|
||||||
|
internalFormSubscription: Subscription;
|
||||||
|
clearInputSubscription: Subscription;
|
||||||
|
internalForm!: FormControl;
|
||||||
|
choices: PlanUser[];
|
||||||
|
loading: boolean = false;
|
||||||
|
|
||||||
|
mentionsConfig = [
|
||||||
|
{
|
||||||
|
triggerCharacter: this.USER_TRIGGER_CHARACTER,
|
||||||
|
getChoiceLabel: (item: PlanUser): string => {
|
||||||
|
return `${this.USER_TRIGGER_CHARACTER}${item?.user?.name}`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
];
|
||||||
|
mentions: ChoiceWithIndices[] = [];
|
||||||
|
searchRegexp = new RegExp('^([-&.\\w]+ *){0,3}$');
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.internalForm = this.formBuilder.control('', Validators.required);
|
||||||
|
this.internalFormSubscription = this.internalForm.valueChanges.subscribe(value => {
|
||||||
|
this.form.setValue(this._formatTextWithSelections(value, this.mentions));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.internalFormSubscription?.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
getDisplayLabel = (item: PlanUser): string => {
|
||||||
|
return item?.user?.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadChoices({searchText, triggerCharacter}
|
||||||
|
: { searchText: string, triggerCharacter: string}
|
||||||
|
): void {
|
||||||
|
|
||||||
|
if (triggerCharacter != this.USER_TRIGGER_CHARACTER) return;
|
||||||
|
|
||||||
|
if (triggerCharacter == this.USER_TRIGGER_CHARACTER) {
|
||||||
|
|
||||||
|
this.choices = this.planUsers?.filter(u => u?.user?.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectedChoicesChange(selections: ChoiceWithIndices[]): void {
|
||||||
|
this.mentions = selections.map((choice: ChoiceWithIndices) => ({
|
||||||
|
...choice,
|
||||||
|
}))
|
||||||
|
|
||||||
|
this.internalForm.updateValueAndValidity();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMenuShow(): void {}
|
||||||
|
|
||||||
|
onMenuHide(): void {
|
||||||
|
this.choices = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _formatTextWithSelections(content: string, selections: ChoiceWithIndices[]): string {
|
||||||
|
|
||||||
|
|
||||||
|
let formattedContent = content;
|
||||||
|
let replaceContentIndex = 0;
|
||||||
|
selections.forEach((selection: ChoiceWithIndices) => {
|
||||||
|
|
||||||
|
const start = selection.indices.start;
|
||||||
|
const end = selection.indices.end;
|
||||||
|
|
||||||
|
const selectionText = content.substring(start, end);
|
||||||
|
const formattedText = `@{{userid:${selection?.choice?.id}}}`;
|
||||||
|
|
||||||
|
const newReplace = formattedContent
|
||||||
|
.substring(replaceContentIndex)
|
||||||
|
.replace(selectionText, formattedText);
|
||||||
|
formattedContent =
|
||||||
|
replaceContentIndex === 0
|
||||||
|
? newReplace
|
||||||
|
: formattedContent.substring(0, replaceContentIndex) + newReplace;
|
||||||
|
|
||||||
|
replaceContentIndex = formattedContent.lastIndexOf('}}') + 2;
|
||||||
|
});
|
||||||
|
|
||||||
|
formattedContent = formattedContent.replace(/\n/g, '<br>');
|
||||||
|
|
||||||
|
return formattedContent;
|
||||||
|
}
|
||||||
|
}
|
|
@ -89,9 +89,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="heading2 col-12">{{'PLAN-EDITOR.FIELDS.DESCRIPTION' | translate}}</div>
|
<div class="heading2 col-12">{{'PLAN-EDITOR.FIELDS.DESCRIPTION' | translate}} *</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<rich-text-editor-component [form]="formGroup.get('description')" placeholder="{{'PLAN-EDITOR.PLACEHOLDER.DESCRIPTION' | translate}}" [required]="false">
|
<rich-text-editor-component [form]="formGroup.get('description')" placeholder="{{'PLAN-EDITOR.PLACEHOLDER.DESCRIPTION' | translate}}" [required]="true">
|
||||||
</rich-text-editor-component>
|
</rich-text-editor-component>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -546,7 +546,7 @@ export class PlanEditorComponent extends BaseEditor<PlanEditorModel, Plan> imple
|
||||||
this.selectedBlueprint = data;
|
this.selectedBlueprint = data;
|
||||||
this.formGroup.get('blueprint').setValue(this.selectedBlueprint.id);
|
this.formGroup.get('blueprint').setValue(this.selectedBlueprint.id);
|
||||||
|
|
||||||
const goToNextStep: boolean = this.formGroup.get('label').valid;
|
const goToNextStep: boolean = this.formGroup.get('label').valid && this.formGroup.get('description').valid;
|
||||||
if (goToNextStep) {
|
if (goToNextStep) {
|
||||||
this.buildFormAfterBlueprintSelection();
|
this.buildFormAfterBlueprintSelection();
|
||||||
this.nextStep();
|
this.nextStep();
|
||||||
|
|
Loading…
Reference in New Issue