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