diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index 17729cba0..bc7104294 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -244,6 +244,7 @@ "common.events": "Events", "common.executed": "Executed", "common.expertMode": "Expert Mode", + "common.extension": "Extension", "common.failed": "Failed", "common.fallback": "Fallback", "common.field": "Field", @@ -656,6 +657,7 @@ "schemas.addNestedField": "Add Nested Field", "schemas.changeCategoryFailed": "Failed to change category. Please reload.", "schemas.clone": "Clone Schema", + "schemas.contentEditorUrl": "Content Editor Extension", "schemas.contentSidebarUrl": "Content Sidebar Extension", "schemas.contentSidebarUrlHint": "URL to the plugin for the sidebar in the details view.", "schemas.contentsSidebarUrl": "Contents Sidebar Extension", diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index 9fb1198ad..d82dd34c2 100644 --- a/backend/i18n/frontend_it.json +++ b/backend/i18n/frontend_it.json @@ -244,6 +244,7 @@ "common.events": "Eventi", "common.executed": "Eseguito", "common.expertMode": "Modalità esperto", + "common.extension": "Extension", "common.failed": "Fallito", "common.fallback": "Alternativa", "common.field": "Campo", @@ -656,6 +657,7 @@ "schemas.addNestedField": "Aggiungi un campo annidato", "schemas.changeCategoryFailed": "Non è stato possibile cambiare la categoria. Per favore ricarica.", "schemas.clone": "Clona lo Schema", + "schemas.contentEditorUrl": "Content Editor Extension", "schemas.contentSidebarUrl": "Estensione della barra di navigazione laterale (contenuti)", "schemas.contentSidebarUrlHint": "URL del plug-in per la barra di navigazione laterale nella visualizzazione dei dettagli.", "schemas.contentsSidebarUrl": "Estensione della barra di navigazione laterale (liste)", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index 636fe31d2..d51c782cf 100644 --- a/backend/i18n/frontend_nl.json +++ b/backend/i18n/frontend_nl.json @@ -244,6 +244,7 @@ "common.events": "Evenementen", "common.executed": "Uitgevoerd", "common.expertMode": "Expert-modus", + "common.extension": "Extension", "common.failed": "Mislukt", "common.fallback": "Fallback", "common.field": "Veld", @@ -656,6 +657,7 @@ "schemas.addNestedField": "Voeg genest veld toe", "schemas.changeCategoryFailed": "Kan categorie niet wijzigen. Laad opnieuw.", "schemas.clone": "Clone Schema", + "schemas.contentEditorUrl": "Content Editor Extension", "schemas.contentSidebarUrl": "Inhoud zijbalk uitbreiding", "schemas.contentSidebarUrlHint": "URL naar de plug-in voor de zijbalk in de detailweergave.", "schemas.contentsSidebarUrl": "Inhoud zijbalk uitbreiding", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index 17729cba0..bc7104294 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -244,6 +244,7 @@ "common.events": "Events", "common.executed": "Executed", "common.expertMode": "Expert Mode", + "common.extension": "Extension", "common.failed": "Failed", "common.fallback": "Fallback", "common.field": "Field", @@ -656,6 +657,7 @@ "schemas.addNestedField": "Add Nested Field", "schemas.changeCategoryFailed": "Failed to change category. Please reload.", "schemas.clone": "Clone Schema", + "schemas.contentEditorUrl": "Content Editor Extension", "schemas.contentSidebarUrl": "Content Sidebar Extension", "schemas.contentSidebarUrlHint": "URL to the plugin for the sidebar in the details view.", "schemas.contentsSidebarUrl": "Contents Sidebar Extension", diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs index 12e7d25e8..9e24082bc 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs @@ -18,6 +18,8 @@ namespace Squidex.Domain.Apps.Core.Schemas public string? ContentSidebarUrl { get; set; } + public string? ContentEditorUrl { get; set; } + public bool ValidateOnPublish { get; set; } } } \ No newline at end of file diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs index e7d7288bd..1184c184a 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs @@ -34,6 +34,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models /// public string? ContentSidebarUrl { get; set; } + /// + /// The url to the editor plugin. + /// + public string? ContentEditorUrl { get; set; } + /// /// True to validate the content items on publish. /// diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs index 9cbadf8ec..2d6601576 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs @@ -37,6 +37,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models /// public string? ContentSidebarUrl { get; set; } + /// + /// The url to the editor plugin. + /// + public string? ContentEditorUrl { get; set; } + /// /// True to validate the content items on publish. /// diff --git a/frontend/app/features/content/declarations.ts b/frontend/app/features/content/declarations.ts index 85b34b406..bd4a97fb3 100644 --- a/frontend/app/features/content/declarations.ts +++ b/frontend/app/features/content/declarations.ts @@ -19,6 +19,7 @@ export * from './pages/contents/contents-page.component'; export * from './pages/contents/custom-view-editor.component'; export * from './pages/schemas/schemas-page.component'; export * from './pages/sidebar/sidebar-page.component'; +export * from './shared/content-extension.component'; export * from './shared/content-status.component'; export * from './shared/due-time-selector.component'; export * from './shared/forms/array-editor.component'; diff --git a/frontend/app/features/content/module.ts b/frontend/app/features/content/module.ts index bbea70e91..15c14c6ba 100644 --- a/frontend/app/features/content/module.ts +++ b/frontend/app/features/content/module.ts @@ -11,6 +11,7 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CanDeactivateGuard, ContentMustExistGuard, LoadLanguagesGuard, SchemaMustExistPublishedGuard, SchemaMustNotBeSingletonGuard, SqxFrameworkModule, SqxSharedModule, UnsetContentGuard } from '@app/shared'; import { ArrayEditorComponent, ArrayItemComponent, ArraySectionComponent, AssetsEditorComponent, CommentsPageComponent, ContentComponent, ContentCreatorComponent, ContentEditorComponent, ContentEventComponent, ContentFieldComponent, ContentHistoryPageComponent, ContentListCellDirective, ContentListFieldComponent, ContentListHeaderComponent, ContentListWidthPipe, ContentPageComponent, ContentReferencesComponent, ContentsColumnsPipe, ContentSectionComponent, ContentSelectorComponent, ContentSelectorItemComponent, ContentsFiltersPageComponent, ContentsPageComponent, ContentStatusComponent, ContentValueComponent, ContentValueEditorComponent, CustomViewEditorComponent, DueTimeSelectorComponent, FieldEditorComponent, FieldLanguagesComponent, PreviewButtonComponent, ReferenceItemComponent, ReferencesEditorComponent, SchemasPageComponent, SidebarPageComponent, StockPhotoEditorComponent } from './declarations'; +import { ContentExtensionComponent } from './shared/content-extension.component'; const routes: Routes = [ { @@ -91,20 +92,21 @@ const routes: Routes = [ ContentCreatorComponent, ContentEditorComponent, ContentEventComponent, + ContentExtensionComponent, ContentFieldComponent, ContentHistoryPageComponent, ContentListCellDirective, ContentListFieldComponent, ContentListHeaderComponent, ContentListWidthPipe, - ContentsColumnsPipe, ContentPageComponent, + ContentReferencesComponent, + ContentsColumnsPipe, ContentSectionComponent, ContentSelectorComponent, ContentSelectorItemComponent, ContentsFiltersPageComponent, ContentsPageComponent, - ContentReferencesComponent, ContentStatusComponent, ContentValueComponent, ContentValueEditorComponent, diff --git a/frontend/app/features/content/pages/content/content-page.component.html b/frontend/app/features/content/pages/content/content-page.component.html index f726474cb..96daaada1 100644 --- a/frontend/app/features/content/pages/content/content-page.component.html +++ b/frontend/app/features/content/pages/content/content-page.component.html @@ -34,6 +34,11 @@ {{ 'contents.contentTab.referencing' | sqxTranslate }} +
  • + + {{ 'common.extension' | sqxTranslate }} + +
  • @@ -113,6 +118,13 @@ [content]="content"> + + + + -
    - -
    + +
    diff --git a/frontend/app/features/content/pages/sidebar/sidebar-page.component.ts b/frontend/app/features/content/pages/sidebar/sidebar-page.component.ts index 32316a362..cf569dee1 100644 --- a/frontend/app/features/content/pages/sidebar/sidebar-page.component.ts +++ b/frontend/app/features/content/pages/sidebar/sidebar-page.component.ts @@ -5,11 +5,11 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Renderer2, ViewChild } from '@angular/core'; -import { Router } from '@angular/router'; -import { ApiUrlConfig, defined, ResourceOwner, Types } from '@app/framework/internal'; -import { AppsState, AuthService, ContentsState, SchemasState } from '@app/shared'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { defined } from '@app/framework/internal'; +import { ContentsState, SchemasState } from '@app/shared'; import { combineLatest } from 'rxjs'; +import { map } from 'rxjs/operators'; @Component({ selector: 'sqx-sidebar-page', @@ -17,97 +17,22 @@ import { combineLatest } from 'rxjs'; templateUrl: './sidebar-page.component.html', changeDetection: ChangeDetectionStrategy.OnPush }) -export class SidebarPageComponent extends ResourceOwner implements AfterViewInit { - private readonly context: any; - private content: any; - private isInitialized = false; - - @ViewChild('iframe', { static: false }) - public iframe: ElementRef; - - constructor(apiUrl: ApiUrlConfig, authService: AuthService, appsState: AppsState, - private readonly contentsState: ContentsState, - private readonly schemasState: SchemasState, - private readonly renderer: Renderer2, - private readonly router: Router +export class SidebarPageComponent { + public url = combineLatest([ + this.schemasState.selectedSchema.pipe(defined()), + this.contentsState.selectedContent + ]).pipe(map(([schema, content]) => { + const url = + content ? + schema.properties.contentSidebarUrl : + schema.properties.contentsSidebarUrl; + + return url; + })); + + constructor( + public readonly contentsState: ContentsState, + public readonly schemasState: SchemasState ) { - super(); - - this.context = { - apiUrl: apiUrl.buildUrl('api'), - appId: appsState.snapshot.selectedApp!.id, - appName: appsState.snapshot.selectedApp!.name, - user: authService.user - }; - } - - public ngAfterViewInit() { - this.own( - combineLatest([ - this.schemasState.selectedSchema.pipe(defined()), - this.contentsState.selectedContent - ]).subscribe(([schema, content]) => { - const url = - content ? - schema.properties.contentSidebarUrl : - schema.properties.contentsSidebarUrl; - - this.context['schemaName'] = schema.name; - this.context['schemaId'] = schema.id; - - this.iframe.nativeElement.src = url || ''; - })); - - this.own( - this.contentsState.selectedContent - .subscribe(content => { - this.content = content; - - this.sendContent(); - })); - - this.own( - this.renderer.listen('window', 'message', (event: MessageEvent) => { - if (event.source === this.iframe.nativeElement.contentWindow) { - const { type } = event.data; - - if (type === 'started') { - this.isInitialized = true; - - this.sendInit(); - this.sendContent(); - } else if (type === 'resize') { - const { height } = event.data; - - this.iframe.nativeElement.height = `${height}px`; - } else if (type === 'navigate') { - const { url } = event.data; - - this.router.navigateByUrl(url); - } - } - })); - } - - private sendInit() { - this.sendMessage('init', { context: this.context }); - } - - private sendContent() { - this.sendMessage('contentChanged', { content: this.content }); - } - - private sendMessage(type: string, payload: any) { - if (!this.iframe) { - return; - } - - const iframe = this.iframe.nativeElement; - - if (this.isInitialized && iframe.contentWindow && Types.isFunction(iframe.contentWindow.postMessage)) { - const message = { type, ...payload }; - - iframe.contentWindow.postMessage(message, '*'); - } } } \ No newline at end of file diff --git a/frontend/app/features/content/shared/content-extension.component.html b/frontend/app/features/content/shared/content-extension.component.html new file mode 100644 index 000000000..0caa93311 --- /dev/null +++ b/frontend/app/features/content/shared/content-extension.component.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/app/features/content/shared/content-extension.component.scss b/frontend/app/features/content/shared/content-extension.component.scss new file mode 100644 index 000000000..ae2678456 --- /dev/null +++ b/frontend/app/features/content/shared/content-extension.component.scss @@ -0,0 +1,5 @@ +iframe { + background: 0; + border: 0; + overflow: hidden; +} \ No newline at end of file diff --git a/frontend/app/features/content/shared/content-extension.component.ts b/frontend/app/features/content/shared/content-extension.component.ts new file mode 100644 index 000000000..f319d4779 --- /dev/null +++ b/frontend/app/features/content/shared/content-extension.component.ts @@ -0,0 +1,111 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, Renderer2, SimpleChanges, ViewChild } from '@angular/core'; +import { Router } from '@angular/router'; +import { ApiUrlConfig, ResourceOwner, Types } from '@app/framework/internal'; +import { AppsState, AuthService, ContentDto, SchemaDto } from '@app/shared'; + +@Component({ + selector: 'sqx-content-extension', + styleUrls: ['./content-extension.component.scss'], + templateUrl: './content-extension.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ContentExtensionComponent extends ResourceOwner implements AfterViewInit, OnChanges { + private readonly context: any; + private isInitialized = false; + + @Input() + public url: string; + + @Input() + public content: ContentDto; + + @Input() + public contentSchema: SchemaDto; + + @ViewChild('iframe', { static: false }) + public iframe: ElementRef; + + constructor(apiUrl: ApiUrlConfig, authService: AuthService, appsState: AppsState, + private readonly renderer: Renderer2, + private readonly router: Router + ) { + super(); + + this.context = { + apiUrl: apiUrl.buildUrl('api'), + appId: appsState.snapshot.selectedApp!.id, + appName: appsState.snapshot.selectedApp!.name, + user: authService.user + }; + } + + public ngOnChanges(changes: SimpleChanges) { + if (changes['url'] && this.iframe?.nativeElement) { + this.iframe.nativeElement.src = this.url || ''; + } + + if (changes['contentSchema']) { + this.context['schemaName'] = this.contentSchema?.name; + this.context['schemaId'] = this.contentSchema?.id; + } + + if (changes['content']) { + this.sendContent(); + } + } + + public ngAfterViewInit() { + this.iframe.nativeElement.src = this.url || ''; + + this.own( + this.renderer.listen('window', 'message', (event: MessageEvent) => { + if (event.source === this.iframe.nativeElement.contentWindow) { + const { type } = event.data; + + if (type === 'started') { + this.isInitialized = true; + + this.sendInit(); + this.sendContent(); + } else if (type === 'resize') { + const { height } = event.data; + + this.iframe.nativeElement.height = `${height}px`; + } else if (type === 'navigate') { + const { url } = event.data; + + this.router.navigateByUrl(url); + } + } + })); + } + + private sendInit() { + this.sendMessage('init', { context: this.context }); + } + + private sendContent() { + this.sendMessage('contentChanged', { content: this.content }); + } + + private sendMessage(type: string, payload: any) { + if (!this.iframe) { + return; + } + + const iframe = this.iframe.nativeElement; + + if (this.isInitialized && iframe.contentWindow && Types.isFunction(iframe.contentWindow.postMessage)) { + const message = { type, ...payload }; + + iframe.contentWindow.postMessage(message, '*'); + } + } +} \ No newline at end of file diff --git a/frontend/app/features/schemas/pages/schema/common/schema-edit-form.component.html b/frontend/app/features/schemas/pages/schema/common/schema-edit-form.component.html index 7e8e63fae..34c875264 100644 --- a/frontend/app/features/schemas/pages/schema/common/schema-edit-form.component.html +++ b/frontend/app/features/schemas/pages/schema/common/schema-edit-form.component.html @@ -50,6 +50,16 @@ {{ 'schemas.contentSidebarUrlHint' | sqxTranslate }} +
    + + + + + + + {{ 'schemas.contentEditorUrl' | sqxTranslate }} +
    +
    diff --git a/frontend/app/shared/services/schemas.service.spec.ts b/frontend/app/shared/services/schemas.service.spec.ts index ca8b09ea6..16976524b 100644 --- a/frontend/app/shared/services/schemas.service.spec.ts +++ b/frontend/app/shared/services/schemas.service.spec.ts @@ -605,6 +605,7 @@ describe('SchemasService', () => { label: `label${id}${suffix}`, contentsSidebarUrl: `url/to/contents/${id}${suffix}`, contentSidebarUrl: `url/to/content/${id}${suffix}`, + contentEditorUrl: `url/to/editor/${id}${suffix}`, tags: [ `tags${id}${suffix}` ], @@ -820,6 +821,7 @@ function createSchemaProperties(id: number, suffix = '') { `hints${id}${suffix}`, `url/to/contents/${id}${suffix}`, `url/to/content/${id}${suffix}`, + `url/to/editor/${id}${suffix}`, id % 2 === 1, [ `tags${id}${suffix}` diff --git a/frontend/app/shared/services/schemas.service.ts b/frontend/app/shared/services/schemas.service.ts index 210696ea7..acba1b29d 100644 --- a/frontend/app/shared/services/schemas.service.ts +++ b/frontend/app/shared/services/schemas.service.ts @@ -327,6 +327,7 @@ export class SchemaPropertiesDto { public readonly hints?: string, public readonly contentsSidebarUrl?: string, public readonly contentSidebarUrl?: string, + public readonly contentEditorUrl?: string, public readonly validateOnPublish?: boolean, public readonly tags?: ReadonlyArray ) { @@ -371,6 +372,7 @@ export interface UpdateSchemaDto { readonly hints?: string; readonly contentsSidebarUrl?: string; readonly contentSidebarUrl?: string; + readonly contentEditorUrl?: string; readonly validateOnPublish?: boolean; readonly tags?: ReadonlyArray; } @@ -753,6 +755,7 @@ function parseProperties(response: any) { response.hints, response.contentsSidebarUrl, response.contentSidebarUrl, + response.contentEditorUrl, response.validateOnPublish, response.tags); } diff --git a/frontend/app/shared/state/schemas.forms.ts b/frontend/app/shared/state/schemas.forms.ts index 8b6a81582..167798acd 100644 --- a/frontend/app/shared/state/schemas.forms.ts +++ b/frontend/app/shared/state/schemas.forms.ts @@ -237,6 +237,7 @@ export class EditSchemaForm extends Form