diff --git a/libs/shared/src/lib/components/widgets/common/html-widget-content/html-widget-content.component.ts b/libs/shared/src/lib/components/widgets/common/html-widget-content/html-widget-content.component.ts index 536abfbe93..4de341962b 100644 --- a/libs/shared/src/lib/components/widgets/common/html-widget-content/html-widget-content.component.ts +++ b/libs/shared/src/lib/components/widgets/common/html-widget-content/html-widget-content.component.ts @@ -1,4 +1,4 @@ -import { Component, Input } from '@angular/core'; +import { Component, ElementRef, Input } from '@angular/core'; import { SafeHtml } from '@angular/platform-browser'; /** @@ -16,4 +16,11 @@ import { SafeHtml } from '@angular/platform-browser'; export class HtmlWidgetContentComponent { @Input() html: SafeHtml = ''; @Input() style?: string; + + /** + * HTML Widget content component + * + * @param {ElementRef} el Element reference + */ + constructor(public el: ElementRef) {} } diff --git a/libs/shared/src/lib/components/widgets/editor/editor.component.ts b/libs/shared/src/lib/components/widgets/editor/editor.component.ts index 0c39259ad6..fb119d60e4 100644 --- a/libs/shared/src/lib/components/widgets/editor/editor.component.ts +++ b/libs/shared/src/lib/components/widgets/editor/editor.component.ts @@ -4,10 +4,11 @@ import { Input, TemplateRef, ViewChild, + HostListener, } from '@angular/core'; import { SafeHtml } from '@angular/platform-browser'; import { Apollo } from 'apollo-angular'; -import { firstValueFrom } from 'rxjs'; +import { firstValueFrom, takeUntil } from 'rxjs'; import { GET_LAYOUT, GET_RESOURCE_METADATA, @@ -26,6 +27,8 @@ import { ReferenceDataQueryResponse, } from '../../../models/reference-data.model'; import { GET_REFERENCE_DATA } from './graphql/queries'; +import { HtmlWidgetContentComponent } from '../common/html-widget-content/html-widget-content.component'; +import { UnsubscribeComponent } from '../../utils/unsubscribe/unsubscribe.component'; /** * Text widget component using KendoUI @@ -35,13 +38,14 @@ import { GET_REFERENCE_DATA } from './graphql/queries'; templateUrl: './editor.component.html', styleUrls: ['./editor.component.scss'], }) -export class EditorComponent implements OnInit { +export class EditorComponent extends UnsubscribeComponent implements OnInit { /** Widget settings */ @Input() settings: any; /** Should show padding */ @Input() usePadding = true; private layout: any; + private record?: any; /** Configured reference data */ private referenceData?: ReferenceData; private fields: any[] = []; @@ -53,6 +57,9 @@ export class EditorComponent implements OnInit { public formattedStyle?: string; @ViewChild('headerTemplate') headerTemplate!: TemplateRef; + /** Reference to html content component */ + @ViewChild(HtmlWidgetContentComponent) + htmlContentComponent!: HtmlWidgetContentComponent; /** @returns does the card use reference data */ get useReferenceData() { @@ -87,7 +94,9 @@ export class EditorComponent implements OnInit { private translate: TranslateService, private gridService: GridService, private referenceDataService: ReferenceDataService - ) {} + ) { + super(); + } /** Sanitizes the text. */ async ngOnInit(): Promise { @@ -132,6 +141,60 @@ export class EditorComponent implements OnInit { } } + /** + * Listen to click events from host element, if record editor is clicked, open record editor modal + * + * @param event Click event from host element + */ + @HostListener('click', ['$event']) + onContentClick(event: any) { + const content = this.htmlContentComponent.el.nativeElement; + const editorTriggers = content.querySelectorAll('.record-editor'); + editorTriggers.forEach((recordEditor: HTMLElement) => { + if (recordEditor.contains(event.target)) { + this.openEditRecordModal(); + } + }); + } + + /** + * Open edit record modal. + */ + private async openEditRecordModal() { + if (this.record && this.record.canUpdate) { + const { FormModalComponent } = await import( + '../../form-modal/form-modal.component' + ); + const dialogRef = this.dialog.open(FormModalComponent, { + disableClose: true, + data: { + recordId: this.record.id, + // template: this.settings.template || null, + template: null, + }, + autoFocus: false, + }); + dialogRef.closed.pipe(takeUntil(this.destroy$)).subscribe((value) => { + if (value) { + // Update the record, based on new configuration + this.getRecord().then(() => { + this.formattedStyle = this.dataTemplateService.renderStyle( + this.settings.wholeCardStyles || false, + this.fieldsValue, + this.styles + ); + this.formattedHtml = this.dataTemplateService.renderHtml( + this.settings.text, + this.fieldsValue, + this.fields, + this.styles + ); + }); + } + }); + } + } + /** * Get reference data. */ @@ -225,8 +288,8 @@ export class EditorComponent implements OnInit { }, }) ); - const record: any = get(res.data, `${queryName}.edges[0].node`, null); - this.fieldsValue = { ...record }; + this.record = get(res.data, `${queryName}.edges[0].node`, null); + this.fieldsValue = { ...this.record }; const metaQuery = this.queryBuilder.buildMetaQuery(this.layout.query); if (metaQuery) { const metaData = await firstValueFrom(metaQuery); diff --git a/libs/shared/src/lib/components/widgets/summary-card/summary-card-item-content/summary-card-item-content.component.ts b/libs/shared/src/lib/components/widgets/summary-card/summary-card-item-content/summary-card-item-content.component.ts index 6153f58bc1..f36379fe1b 100644 --- a/libs/shared/src/lib/components/widgets/summary-card/summary-card-item-content/summary-card-item-content.component.ts +++ b/libs/shared/src/lib/components/widgets/summary-card/summary-card-item-content/summary-card-item-content.component.ts @@ -1,12 +1,20 @@ import { Component, + HostListener, Input, OnChanges, OnInit, + Optional, + ViewChild, ViewEncapsulation, } from '@angular/core'; import { SafeHtml } from '@angular/platform-browser'; import { DataTemplateService } from '../../../../services/data-template/data-template.service'; +import { UnsubscribeComponent } from '../../../utils/unsubscribe/unsubscribe.component'; +import { Dialog } from '@angular/cdk/dialog'; +import { HtmlWidgetContentComponent } from '../../common/html-widget-content/html-widget-content.component'; +import { takeUntil } from 'rxjs'; +import { SummaryCardItemComponent } from '../summary-card-item/summary-card-item.component'; /** * Content component of Single Item of Summary Card. @@ -18,22 +26,42 @@ import { DataTemplateService } from '../../../../services/data-template/data-tem styleUrls: ['./summary-card-item-content.component.scss'], encapsulation: ViewEncapsulation.None, }) -export class SummaryCardItemContentComponent implements OnInit, OnChanges { +export class SummaryCardItemContentComponent + extends UnsubscribeComponent + implements OnInit, OnChanges +{ + /** Html template */ @Input() html = ''; + /** Available fields */ @Input() fields: any[] = []; + /** Fields value */ @Input() fieldsValue: any; + /** Styles to load */ @Input() styles: any[] = []; + /** Should style apply to whole card */ @Input() wholeCardStyles = false; - + /** Reference to html content component */ + @ViewChild(HtmlWidgetContentComponent) + htmlContentComponent!: HtmlWidgetContentComponent; + /** Formatted html, to be rendered */ public formattedHtml: SafeHtml = ''; + /** Formatted style, to be applied */ public formattedStyle?: string; /** * Content component of Single Item of Summary Card. * * @param dataTemplateService Shared data template service, used to render content from template + * @param dialog CDK Dialog service + * @param parent Reference to parent summary card item component */ - constructor(private dataTemplateService: DataTemplateService) {} + constructor( + private dataTemplateService: DataTemplateService, + private dialog: Dialog, + @Optional() public parent: SummaryCardItemComponent + ) { + super(); + } ngOnInit(): void { this.formattedStyle = this.dataTemplateService.renderStyle( @@ -66,6 +94,22 @@ export class SummaryCardItemContentComponent implements OnInit, OnChanges { ); } + /** + * Listen to click events from host element, if record editor is clicked, open record editor modal + * + * @param event Click event from host element + */ + @HostListener('click', ['$event']) + onContentClick(event: any) { + const content = this.htmlContentComponent.el.nativeElement; + const editorTriggers = content.querySelectorAll('.record-editor'); + editorTriggers.forEach((recordEditor: HTMLElement) => { + if (recordEditor.contains(event.target)) { + this.openEditRecordModal(); + } + }); + } + /** * Pass click event to data template service * @@ -74,4 +118,46 @@ export class SummaryCardItemContentComponent implements OnInit, OnChanges { public onClick(event: any) { this.dataTemplateService.onClick(event, this.fieldsValue); } + + /** + * Open edit record modal. + */ + private async openEditRecordModal() { + if ( + this.parent.card.resource && + this.parent.card.layout && + this.fieldsValue.canUpdate + ) { + const { FormModalComponent } = await import( + '../../../form-modal/form-modal.component' + ); + const dialogRef = this.dialog.open(FormModalComponent, { + disableClose: true, + data: { + recordId: this.fieldsValue.id, + template: this.parent.card.template, + }, + autoFocus: false, + }); + dialogRef.closed.pipe(takeUntil(this.destroy$)).subscribe((value) => { + if (value) { + this.parent.refresh(); + // Update the record, based on new configuration + // this.getRecord().then(() => { + // this.formattedStyle = this.dataTemplateService.renderStyle( + // this.settings.wholeCardStyles || false, + // this.fieldsValue, + // this.styles + // ); + // this.formattedHtml = this.dataTemplateService.renderHtml( + // this.settings.text, + // this.fieldsValue, + // this.fields, + // this.styles + // ); + // }); + } + }); + } + } } diff --git a/libs/shared/src/lib/components/widgets/summary-card/summary-card-item/summary-card-item.component.ts b/libs/shared/src/lib/components/widgets/summary-card/summary-card-item/summary-card-item.component.ts index c4537513ed..9de584e17c 100644 --- a/libs/shared/src/lib/components/widgets/summary-card/summary-card-item/summary-card-item.component.ts +++ b/libs/shared/src/lib/components/widgets/summary-card/summary-card-item/summary-card-item.component.ts @@ -1,6 +1,6 @@ -import { Component, Input, OnChanges, OnInit } from '@angular/core'; +import { Component, Input, OnChanges, OnInit, Optional } from '@angular/core'; import { get } from 'lodash'; -import { CardT } from '../summary-card.component'; +import { CardT, SummaryCardComponent } from '../summary-card.component'; /** * Single Item component of Summary card widget. @@ -25,6 +25,13 @@ export class SummaryCardItemComponent implements OnInit, OnChanges { return get(this.card, 'usePadding') ?? true; } + /** + * Single Item component of Summary card widget. + * + * @param parent Reference to parent summary card component + */ + constructor(@Optional() public parent: SummaryCardComponent) {} + ngOnInit(): void { this.setContent(); } @@ -57,13 +64,13 @@ export class SummaryCardItemComponent implements OnInit, OnChanges { * Set content of the card item, querying associated record. */ private async setContentFromLayout(): Promise { - await this.getStyles(); + this.getStyles(); this.fieldsValue = { ...this.card.record }; this.fields = this.card.metadata || []; } /** Sets layout style */ - private async getStyles(): Promise { + private getStyles(): void { this.styles = get(this.card.layout, 'query.style', []); } @@ -79,4 +86,11 @@ export class SummaryCardItemComponent implements OnInit, OnChanges { editor: 'text', })); } + + /** + * Refresh card + */ + public refresh() { + this.parent.refresh(); + } } diff --git a/libs/shared/src/lib/components/widgets/summary-card/summary-card.component.ts b/libs/shared/src/lib/components/widgets/summary-card/summary-card.component.ts index cf9fbce18a..78a250177b 100644 --- a/libs/shared/src/lib/components/widgets/summary-card/summary-card.component.ts +++ b/libs/shared/src/lib/components/widgets/summary-card/summary-card.component.ts @@ -259,13 +259,7 @@ export class SummaryCardComponent this.contextService.filter$ .pipe(debounceTime(500), takeUntil(this.destroy$)) .subscribe(() => { - this.onPage({ - pageSize: DEFAULT_PAGE_SIZE, - skip: 0, - previousPageIndex: 0, - pageIndex: 0, - totalItems: 0, - }); + this.refresh(); }); } @@ -730,6 +724,19 @@ export class SummaryCardComponent } } + /** + * Refresh view + */ + public refresh() { + this.onPage({ + pageSize: DEFAULT_PAGE_SIZE, + skip: 0, + previousPageIndex: 0, + pageIndex: 0, + totalItems: 0, + }); + } + /** * Open the dataSource modal. */ diff --git a/libs/shared/src/lib/const/tinymce.const.ts b/libs/shared/src/lib/const/tinymce.const.ts index 711b5ee4d8..316a15901f 100644 --- a/libs/shared/src/lib/const/tinymce.const.ts +++ b/libs/shared/src/lib/const/tinymce.const.ts @@ -1,4 +1,5 @@ import { RawEditorSettings } from 'tinymce'; +import { createFontAwesomeIcon } from '../components/ui/map/utils/create-div-icon'; /** Language tinymce keys paired with the default ones */ export const EDITOR_LANGUAGE_PAIRS: { key: string; tinymceKey: string }[] = [ @@ -16,7 +17,7 @@ export const WIDGET_EDITOR_CONFIG: RawEditorSettings = { imagetools_cors_hosts: ['picsum.photos'], menubar: 'edit view insert format tools table help', toolbar: - 'undo redo | bold italic underline strikethrough | fontselect fontsizeselect formatselect | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist | forecolor backcolor removeformat | charmap emoticons | fullscreen preview save | insertfile image media link avatar', + 'undo redo | bold italic underline strikethrough | fontselect fontsizeselect formatselect | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist | forecolor backcolor removeformat | charmap emoticons | fullscreen preview save | insertfile image media link avatar recordeditor', toolbar_sticky: true, image_advtab: true, importcss_append: true, @@ -31,8 +32,46 @@ export const WIDGET_EDITOR_CONFIG: RawEditorSettings = { 'shortcuts', // the default shortcuts tab 'keyboardnav', // the default keyboard navigation tab ], + extended_valid_elements: + 'a[*],altGlyph[*],altGlyphDef[*],altGlyphItem[*],animate[*],animateMotion[*],animateTransform[*],circle[*],clipPath[*],color-profile[*],cursor[*],defs[*],desc[*],ellipse[*],feBlend[*],feColorMatrix[*],feComponentTransfer[*],feComposite[*],feConvolveMatrix[*],feDiffuseLighting[*],feDisplacementMap[*],feDistantLight[*],feFlood[*],feFuncA[*],feFuncB[*],feFuncG[*],feFuncR[*],feGaussianBlur[*],feImage[*],feMerge[*],feMergeNode[*],feMorphology[*],feOffset[*],fePointLight[*],feSpecularLighting[*],feSpotLight[*],feTile[*],feTurbulence[*],filter[*],font[*],font-face[*],font-face-format[*],font-face-name[*],font-face-src[*],font-face-uri[*],foreignObject[*],g[*],glyph[*],glyphRef[*],hkern[*],image[*],line[*],linearGradient[*],marker[*],mask[*],metadata[*],missing-glyph[*],mpath[*],path[*],pattern[*],polygon[*],polyline[*],radialGradient[*],rect[*],script[*],set[*],stop[*],style[*],svg[*],switch[*],symbol[*],text[*],textPath[*],title[*],tref[*],tspan[*],use[*],view[*],vkern[*],a[*],animate[*],animateMotion[*],animateTransform[*],circle[*],clipPath[*],defs[*],desc[*],discard[*],ellipse[*],feBlend[*],feColorMatrix[*],feComponentTransfer[*],feComposite[*],feConvolveMatrix[*],feDiffuseLighting[*],feDisplacementMap[*],feDistantLight[*],feDropShadow[*],feFlood[*],feFuncA[*],feFuncB[*],feFuncG[*],feFuncR[*],feGaussianBlur[*],feImage[*],feMerge[*],feMergeNode[*],feMorphology[*],feOffset[*],fePointLight[*],feSpecularLighting[*],feSpotLight[*],feTile[*],feTurbulence[*],filter[*],foreignObject[*],g[*],hatch[*],hatchpath[*],image[*],line[*],linearGradient[*],marker[*],mask[*],metadata[*],mpath[*],path[*],pattern[*],polygon[*],polyline[*],radialGradient[*],rect[*],script[*],set[*],stop[*],style[*],svg[*],switch[*],symbol[*],text[*],textPath[*],title[*],tspan[*],use[*],view[*],g[*],animate[*],animateColor[*],animateMotion[*],animateTransform[*],discard[*],mpath[*],set[*],circle[*],ellipse[*],line[*],polygon[*],polyline[*],rect[*],a[*],defs[*],g[*],marker[*],mask[*],missing-glyph[*],pattern[*],svg[*],switch[*],symbol[*],desc[*],metadata[*],title[*],feBlend[*],feColorMatrix[*],feComponentTransfer[*],feComposite[*],feConvolveMatrix[*],feDiffuseLighting[*],feDisplacementMap[*],feDropShadow[*],feFlood[*],feFuncA[*],feFuncB[*],feFuncG[*],feFuncR[*],feGaussianBlur[*],feImage[*],feMerge[*],feMergeNode[*],feMorphology[*],feOffset[*],feSpecularLighting[*],feTile[*],feTurbulence[*],font[*],font-face[*],font-face-format[*],font-face-name[*],font-face-src[*],font-face-uri[*],hkern[*],vkern[*],linearGradient[*],radialGradient[*],stop[*],circle[*],ellipse[*],image[*],line[*],path[*],polygon[*],polyline[*],rect[*],text[*],use[*],use[*],feDistantLight[*],fePointLight[*],feSpotLight[*],clipPath[*],defs[*],hatch[*],linearGradient[*],marker[*],mask[*],metadata[*],pattern[*],radialGradient[*],script[*],style[*],symbol[*],title[*],hatch[*],linearGradient[*],pattern[*],radialGradient[*],solidcolor[*],a[*],circle[*],ellipse[*],foreignObject[*],g[*],image[*],line[*],path[*],polygon[*],polyline[*],rect[*],svg[*],switch[*],symbol[*],text[*],textPath[*],tspan[*],use[*],g[*],circle[*],ellipse[*],line[*],path[*],polygon[*],polyline[*],rect[*],defs[*],g[*],svg[*],symbol[*],use[*],altGlyph[*],altGlyphDef[*],altGlyphItem[*],glyph[*],glyphRef[*],textPath[*],text[*],tref[*],tspan[*],altGlyph[*],textPath[*],tref[*],tspan[*],clipPath[*],cursor[*],filter[*],foreignObject[*],hatchpath[*],script[*],style[*],view[*],altGlyph[*],altGlyphDef[*],altGlyphItem[*],animateColor[*],cursor[*],font[*],font-face[*],font-face-format[*],font-face-name[*],font-face-src[*],font-face-uri[*],glyph[*],glyphRef[*],hkern[*],missing-glyph[*],tref[*],vkern[*]', convert_urls: false, setup: (editor) => { + // Record edition + const iconEdit = createFontAwesomeIcon( + { + icon: 'edit', + color: 'none', + opacity: 1, + size: 21, + }, + (editor.editorManager as any).DOM.doc + ); + editor.ui.registry.addIcon('record-icon', iconEdit.innerHTML); + editor.ui.registry.addButton('recordeditor', { + icon: 'record-icon', + tooltip: 'Edit record', + onAction: async () => { + const iconEdit = createFontAwesomeIcon( + { + icon: 'edit', + color: '#000000', + opacity: 1, + size: 21, + }, + (editor.editorManager as any).DOM.doc + ); + const iconButton = (editor.editorManager as any).DOM.doc.createElement( + 'button' + ); + iconButton.innerHTML = iconEdit?.innerHTML; + iconButton.style.border = '0'; + iconButton.style.padding = '0'; + iconButton.classList.add('record-editor'); + const set = `${iconButton?.outerHTML} `; + editor.insertContent(set); + }, + }); + // Avatar editor.ui.registry.addIcon( 'avatar-icon', ''