Browse Source

Editor extension.

pull/662/head
Sebastian 5 years ago
parent
commit
e17ec4fc2e
  1. 2
      backend/i18n/frontend_en.json
  2. 2
      backend/i18n/frontend_it.json
  3. 2
      backend/i18n/frontend_nl.json
  4. 2
      backend/i18n/source/frontend_en.json
  5. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs
  6. 5
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs
  7. 5
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs
  8. 1
      frontend/app/features/content/declarations.ts
  9. 6
      frontend/app/features/content/module.ts
  10. 12
      frontend/app/features/content/pages/content/content-page.component.html
  11. 8
      frontend/app/features/content/pages/sidebar/sidebar-page.component.html
  12. 99
      frontend/app/features/content/pages/sidebar/sidebar-page.component.ts
  13. 1
      frontend/app/features/content/shared/content-extension.component.html
  14. 5
      frontend/app/features/content/shared/content-extension.component.scss
  15. 111
      frontend/app/features/content/shared/content-extension.component.ts
  16. 10
      frontend/app/features/schemas/pages/schema/common/schema-edit-form.component.html
  17. 2
      frontend/app/shared/services/schemas.service.spec.ts
  18. 3
      frontend/app/shared/services/schemas.service.ts
  19. 1
      frontend/app/shared/state/schemas.forms.ts

2
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",

2
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)",

2
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",

2
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",

2
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; }
}
}

5
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs

@ -34,6 +34,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
/// </summary>
public string? ContentSidebarUrl { get; set; }
/// <summary>
/// The url to the editor plugin.
/// </summary>
public string? ContentEditorUrl { get; set; }
/// <summary>
/// True to validate the content items on publish.
/// </summary>

5
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs

@ -37,6 +37,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
/// </summary>
public string? ContentSidebarUrl { get; set; }
/// <summary>
/// The url to the editor plugin.
/// </summary>
public string? ContentEditorUrl { get; set; }
/// <summary>
/// True to validate the content items on publish.
/// </summary>

1
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';

6
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,

12
frontend/app/features/content/pages/content/content-page.component.html

@ -34,6 +34,11 @@
{{ 'contents.contentTab.referencing' | sqxTranslate }}
</a>
</li>
<li *ngIf="schema.properties.contentEditorUrl">
<a class="nav-link" [routerLink]="[]" [queryParams]="{ tab: 'extension' }" [class.active]="tab === 'extension'">
{{ 'common.extension' | sqxTranslate }}
</a>
</li>
</ul>
</ng-container>
</ng-container>
@ -113,6 +118,13 @@
[content]="content">
</sqx-content-references>
</ng-container>
<ng-container *ngSwitchCase="'extension'">
<sqx-content-extension mode="referencing" *ngIf="schema.properties.contentEditorUrl && content"
[content]="content"
[contentSchema]="schema"
[url]="schema.properties.contentEditorUrl">
</sqx-content-extension>
</ng-container>
<ng-container *ngSwitchDefault>
<sqx-content-editor
[(language)]="language"

8
frontend/app/features/content/pages/sidebar/sidebar-page.component.html

@ -4,9 +4,11 @@
</ng-container>
<ng-container content>
<div>
<iframe #iframe scrolling="no" width="100%"></iframe>
</div>
<sqx-content-extension
[url]="url | async"
[content]="contentsState.selectedContent | async"
[contentSchema]="schemasState.selectedSchema | async">
</sqx-content-extension>
</ng-container>
</sqx-panel>

99
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<HTMLIFrameElement>;
constructor(apiUrl: ApiUrlConfig, authService: AuthService, appsState: AppsState,
private readonly contentsState: ContentsState,
private readonly schemasState: SchemasState,
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 ngAfterViewInit() {
this.own(
combineLatest([
export class SidebarPageComponent {
public url = combineLatest([
this.schemasState.selectedSchema.pipe(defined()),
this.contentsState.selectedContent
]).subscribe(([schema, content]) => {
]).pipe(map(([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);
}
}
return 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, '*');
}
constructor(
public readonly contentsState: ContentsState,
public readonly schemasState: SchemasState
) {
}
}

1
frontend/app/features/content/shared/content-extension.component.html

@ -0,0 +1 @@
<iframe #iframe scrolling="no" width="100%"></iframe>

5
frontend/app/features/content/shared/content-extension.component.scss

@ -0,0 +1,5 @@
iframe {
background: 0;
border: 0;
overflow: hidden;
}

111
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<HTMLIFrameElement>;
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, '*');
}
}
}

10
frontend/app/features/schemas/pages/schema/common/schema-edit-form.component.html

@ -50,6 +50,16 @@
<sqx-form-hint>{{ 'schemas.contentSidebarUrlHint' | sqxTranslate }}</sqx-form-hint>
</div>
<div class="form-group">
<label for="hints">{{ 'schemas.contentEditorUrl' | sqxTranslate }}</label>
<sqx-control-errors for="contentEditorUrl"></sqx-control-errors>
<input type="url" class="form-control" id="contentEditorUrl" formControlName="contentEditorUrl">
<sqx-form-hint>{{ 'schemas.contentEditorUrl' | sqxTranslate }}</sqx-form-hint>
</div>
<div class="form-group">
<label for="tags">{{ 'common.tags' | sqxTranslate }}</label>

2
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}`

3
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<string>
) {
@ -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<string>;
}
@ -753,6 +755,7 @@ function parseProperties(response: any) {
response.hints,
response.contentsSidebarUrl,
response.contentSidebarUrl,
response.contentEditorUrl,
response.validateOnPublish,
response.tags);
}

1
frontend/app/shared/state/schemas.forms.ts

@ -237,6 +237,7 @@ export class EditSchemaForm extends Form<FormGroup, UpdateSchemaDto, SchemaPrope
],
contentsSidebarUrl: '',
contentSidebarUrl: '',
contentEditorUrl: '',
validateOnPublish: false,
tags: []
}));

Loading…
Cancel
Save