Browse Source

Plugin for contents list.

pull/1044/head
Sebastian 3 years ago
parent
commit
ce17656575
  1. 3
      backend/i18n/frontend_en.json
  2. 3
      backend/i18n/frontend_fr.json
  3. 3
      backend/i18n/frontend_it.json
  4. 3
      backend/i18n/frontend_nl.json
  5. 3
      backend/i18n/frontend_pt.json
  6. 3
      backend/i18n/frontend_zh.json
  7. 3
      backend/i18n/source/frontend_en.json
  8. 42
      backend/i18n/translator/Squidex.Translator/Commands.cs
  9. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs
  10. 4
      backend/src/Squidex.Shared/Texts.fr.resx
  11. 4
      backend/src/Squidex.Shared/Texts.it.resx
  12. 4
      backend/src/Squidex.Shared/Texts.nl.resx
  13. 4
      backend/src/Squidex.Shared/Texts.pt.resx
  14. 4
      backend/src/Squidex.Shared/Texts.resx
  15. 4
      backend/src/Squidex.Shared/Texts.zh.resx
  16. 10
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs
  17. 4
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs
  18. 2
      backend/src/Squidex/wwwroot/scripts/editor-sdk.js
  19. 13
      backend/src/Squidex/wwwroot/scripts/sidebar-content.html
  20. 6
      backend/src/Squidex/wwwroot/scripts/sidebar-context.html
  21. 4
      frontend/src/app/features/content/pages/content/content-page.component.html
  22. 10
      frontend/src/app/features/content/pages/contents-plugin/contents-plugin.component.html
  23. 13
      frontend/src/app/features/content/pages/contents-plugin/contents-plugin.component.scss
  24. 33
      frontend/src/app/features/content/pages/contents-plugin/contents-plugin.component.ts
  25. 7
      frontend/src/app/features/content/pages/sidebar/sidebar-page.component.html
  26. 13
      frontend/src/app/features/content/pages/sidebar/sidebar-page.component.scss
  27. 8
      frontend/src/app/features/content/routes.ts
  28. 2
      frontend/src/app/features/content/shared/content-extension.component.html
  29. 9
      frontend/src/app/features/content/shared/content-extension.component.ts
  30. 12
      frontend/src/app/features/schemas/pages/schema/common/schema-edit-form.component.html
  31. 57
      frontend/src/app/shared/guards/schema-must-not-be-singleton.guard.spec.ts
  32. 44
      frontend/src/app/shared/guards/schema-must-not-be-singleton.guard.ts
  33. 2
      frontend/src/app/shared/services/schemas.service.spec.ts
  34. 5
      frontend/src/app/shared/services/schemas.service.ts
  35. 3
      frontend/src/app/shared/state/schemas.forms.ts

3
backend/i18n/frontend_en.json

@ -799,8 +799,11 @@
"schemas.changeCategoryFailed": "Failed to change category. Please reload.",
"schemas.clone": "Clone Schema",
"schemas.contentEditorUrl": "Content Editor Extension",
"schemas.contentEditorUrlHint": "URL to the plugin to replace the default content editor.",
"schemas.contentSidebarUrl": "Content Sidebar Extension",
"schemas.contentSidebarUrlHint": "URL to the plugin for the sidebar in the details view.",
"schemas.contentsListUrl": "Contents List Extension",
"schemas.contentsListUrlHint": "URL to the plugin to replace the default content list view.",
"schemas.contentsSidebarUrl": "Contents Sidebar Extension",
"schemas.contentsSidebarUrlHint": "URL to the plugin for the sidebar in the list view.",
"schemas.create": "Create Schema",

3
backend/i18n/frontend_fr.json

@ -799,8 +799,11 @@
"schemas.changeCategoryFailed": "Impossible de changer de catégorie. Veuillez recharger.",
"schemas.clone": "Cloner le schéma",
"schemas.contentEditorUrl": "Extension de l'éditeur de contenu",
"schemas.contentEditorUrlHint": "URL to the plugin to replace the default content editor.",
"schemas.contentSidebarUrl": "Extension de la barre latérale de contenu",
"schemas.contentSidebarUrlHint": "URL du plug-in pour la barre latérale dans la vue détaillée.",
"schemas.contentsListUrl": "Contents List Extension",
"schemas.contentsListUrlHint": "URL to the plugin to replace the default content list view.",
"schemas.contentsSidebarUrl": "Contenu de l'extension de la barre latérale",
"schemas.contentsSidebarUrlHint": "URL du plug-in pour la barre latérale dans la vue de liste.",
"schemas.create": "Créer un schéma",

3
backend/i18n/frontend_it.json

@ -799,8 +799,11 @@
"schemas.changeCategoryFailed": "Non è stato possibile cambiare la categoria. Per favore ricarica.",
"schemas.clone": "Clona lo Schema",
"schemas.contentEditorUrl": "Content Editor Extension",
"schemas.contentEditorUrlHint": "URL to the plugin to replace the default content editor.",
"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.contentsListUrl": "Contents List Extension",
"schemas.contentsListUrlHint": "URL to the plugin to replace the default content list view.",
"schemas.contentsSidebarUrl": "Estensione della barra di navigazione laterale (liste)",
"schemas.contentsSidebarUrlHint": "URL del plug-in per la barra di navigazione laterale nella visualizzazione delle liste.",
"schemas.create": "Crea uno Schema",

3
backend/i18n/frontend_nl.json

@ -799,8 +799,11 @@
"schemas.changeCategoryFailed": "Kan categorie niet wijzigen. Laad opnieuw.",
"schemas.clone": "Clone Schema",
"schemas.contentEditorUrl": "Inhoud Editor uitbreiding",
"schemas.contentEditorUrlHint": "URL to the plugin to replace the default content editor.",
"schemas.contentSidebarUrl": "Inhoud zijbalk uitbreiding",
"schemas.contentSidebarUrlHint": "URL naar de plug-in voor de zijbalk in de detailweergave.",
"schemas.contentsListUrl": "Contents List Extension",
"schemas.contentsListUrlHint": "URL to the plugin to replace the default content list view.",
"schemas.contentsSidebarUrl": "Inhoud zijbalk uitbreiding",
"schemas.contentsSidebarUrlHint": "URL naar de plug-in voor de zijbalk in de lijstweergave.",
"schemas.create": "Schema maken",

3
backend/i18n/frontend_pt.json

@ -799,8 +799,11 @@
"schemas.changeCategoryFailed": "Falhou em mudar de categoria. Por favor, recarregue.",
"schemas.clone": "Clonar Esquema",
"schemas.contentEditorUrl": "Extensão do editor de conteúdo",
"schemas.contentEditorUrlHint": "URL to the plugin to replace the default content editor.",
"schemas.contentSidebarUrl": "Extensão da barra lateral de conteúdo",
"schemas.contentSidebarUrlHint": "URL para o plugin para a barra lateral na vista de detalhes.",
"schemas.contentsListUrl": "Contents List Extension",
"schemas.contentsListUrlHint": "URL to the plugin to replace the default content list view.",
"schemas.contentsSidebarUrl": "Extensão da barra lateral de conteúdo",
"schemas.contentsSidebarUrlHint": "URL para o plugin para a barra lateral na vista da lista.",
"schemas.create": "Criar Esquema",

3
backend/i18n/frontend_zh.json

@ -799,8 +799,11 @@
"schemas.changeCategoryFailed": "更改类别失败。请重新加载。",
"schemas.clone": "Clone Schema",
"schemas.contentEditorUrl": "内容编辑器扩展",
"schemas.contentEditorUrlHint": "URL to the plugin to replace the default content editor.",
"schemas.contentSidebarUrl": "内容侧边栏扩展",
"schemas.contentSidebarUrlHint": "详细信息视图中侧边栏插件的 URL。",
"schemas.contentsListUrl": "Contents List Extension",
"schemas.contentsListUrlHint": "URL to the plugin to replace the default content list view.",
"schemas.contentsSidebarUrl": "内容侧边栏扩展",
"schemas.contentsSidebarUrlHint": "列表视图中侧边栏插件的 URL。",
"schemas.create": "Create Schema",

3
backend/i18n/source/frontend_en.json

@ -799,8 +799,11 @@
"schemas.changeCategoryFailed": "Failed to change category. Please reload.",
"schemas.clone": "Clone Schema",
"schemas.contentEditorUrl": "Content Editor Extension",
"schemas.contentEditorUrlHint": "URL to the plugin to replace the default content editor.",
"schemas.contentSidebarUrl": "Content Sidebar Extension",
"schemas.contentSidebarUrlHint": "URL to the plugin for the sidebar in the details view.",
"schemas.contentsListUrl": "Contents List Extension",
"schemas.contentsListUrlHint": "URL to the plugin to replace the default content list view.",
"schemas.contentsSidebarUrl": "Contents Sidebar Extension",
"schemas.contentsSidebarUrlHint": "URL to the plugin for the sidebar in the list view.",
"schemas.create": "Create Schema",

42
backend/i18n/translator/Squidex.Translator/Commands.cs

@ -17,7 +17,7 @@ namespace Squidex.Translator;
public class Commands
{
[Command(Name = "info", Description = "Shows information about the translator.")]
[Command("info", Description = "Shows information about the translator.")]
public void Info()
{
var version = typeof(Commands).Assembly.GetName().Version;
@ -25,11 +25,11 @@ public class Commands
Console.WriteLine($"Squidex Translator Version v{version}");
}
[Command(Name = "translate", Description = "Translates different parts.")]
[SubCommand]
[Command("translate", Description = "Translates different parts.")]
[Subcommand]
public class Translate
{
[Command(Name = "check-backend", Description = "Check backend files.")]
[Command("check-backend", Description = "Check backend files.")]
public void CheckBackend(TranslateArguments arguments)
{
var (folder, service) = Setup(arguments, "backend");
@ -37,7 +37,7 @@ public class Commands
new CheckBackend(folder, service).Run();
}
[Command(Name = "check-frontend", Description = "Check frontend files.")]
[Command("check-frontend", Description = "Check frontend files.")]
public void CheckFrontend(TranslateArguments arguments)
{
var (folder, service) = Setup(arguments, "frontend");
@ -45,7 +45,7 @@ public class Commands
new CheckFrontend(folder, service).Run(arguments.Fix);
}
[Command(Name = "backend", Description = "Translate backend files.")]
[Command("backend", Description = "Translate backend files.")]
public void Backend(TranslateArguments arguments)
{
var (folder, service) = Setup(arguments, "backend");
@ -53,7 +53,7 @@ public class Commands
new TranslateBackend(folder, service).Run();
}
[Command(Name = "templates", Description = "Translate angular templates.")]
[Command("templates", Description = "Translate angular templates.")]
public void Templates(TranslateArguments arguments)
{
var (folder, service) = Setup(arguments, "frontend");
@ -61,7 +61,7 @@ public class Commands
new TranslateTemplates(folder, service).Run(arguments.Report);
}
[Command(Name = "typescript", Description = "Translate typescript files.")]
[Command("typescript", Description = "Translate typescript files.")]
public void Typescript(TranslateArguments arguments)
{
var (folder, service) = Setup(arguments, "frontend");
@ -69,7 +69,7 @@ public class Commands
new TranslateTypescript(folder, service).Run();
}
[Command(Name = "gen-backend", Description = "Generate the backend translations.")]
[Command("gen-backend", Description = "Generate the backend translations.")]
public void GenerateBackend(TranslateArguments arguments)
{
var (folder, service) = Setup(arguments, "backend");
@ -77,7 +77,7 @@ public class Commands
new GenerateBackendResources(folder, service).Run();
}
[Command(Name = "gen-frontend", Description = "Generate the frontend translations.")]
[Command("gen-frontend", Description = "Generate the frontend translations.")]
public void GenerateFrontend(TranslateArguments arguments)
{
var (folder, service) = Setup(arguments, "frontend");
@ -85,7 +85,7 @@ public class Commands
new GenerateFrontendResources(folder, service).Run();
}
[Command(Name = "clean-backend", Description = "Clean the backend translations.")]
[Command("clean-backend", Description = "Clean the backend translations.")]
public void CleanBackend(TranslateArguments arguments)
{
var (_, service) = Setup(arguments, "backend");
@ -95,7 +95,7 @@ public class Commands
service.Save();
}
[Command(Name = "clean-frontend", Description = "Clean the frontend translations.")]
[Command("clean-frontend", Description = "Clean the frontend translations.")]
public void CleanFrontend(TranslateArguments arguments)
{
var (_, service) = Setup(arguments, "frontend");
@ -105,7 +105,7 @@ public class Commands
service.Save();
}
[Command(Name = "gen-keys", Description = "Generate the keys for translations.")]
[Command("gen-keys", Description = "Generate the keys for translations.")]
public void GenerateBackendKeys(TranslateArguments arguments)
{
var (backendFolder, serviceBackend) = Setup(arguments, "backend");
@ -117,7 +117,7 @@ public class Commands
new GenerateKeys(frontendFolder, frontendService, "frontend_keys.json").Run();
}
[Command(Name = "migrate-backend", Description = "Migrate the backend files.")]
[Command("migrate-backend", Description = "Migrate the backend files.")]
public void MigrateBackend(TranslateArguments arguments)
{
var (_, service) = Setup(arguments, "backend");
@ -125,7 +125,7 @@ public class Commands
service.Migrate();
}
[Command(Name = "migrate-frontend", Description = "Migrate the frontend files.")]
[Command("migrate-frontend", Description = "Migrate the frontend files.")]
public void MigrateFrontend(TranslateArguments arguments)
{
var (_, service) = Setup(arguments, "frontend");
@ -164,20 +164,20 @@ public class Commands
[Validator(typeof(Validator))]
public sealed class TranslateArguments : IArgumentModel
{
[Operand(Name = "folder", Description = "The squidex folder.")]
[Operand("folder", Description = "The squidex folder.")]
public string Folder { get; set; }
[Option(LongName = "single", ShortName = "s", Description = "Single words only.")]
[Option('s', "single", Description = "Single words only.")]
public bool SingleWords { get; set; }
[Option(LongName = "report", ShortName = "r")]
[Option('r', "report")]
public bool Report { get; set; }
[Option(LongName = "fix")]
[Option("fix")]
public bool Fix { get; set; }
[Option(LongName = "locale", ShortName = "l")]
public IEnumerable<string> Locales { get; set; }
[Option('l', "locale")]
public IEnumerable<string> Locales { get; set; } = Array.Empty<string>();
public sealed class Validator : AbstractValidator<TranslateArguments>
{

2
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs

@ -21,5 +21,7 @@ public sealed record SchemaProperties : NamedElementPropertiesBase
public string? ContentEditorUrl { get; init; }
public string? ContentsListUrl { get; init; }
public bool ValidateOnPublish { get; init; }
}

4
backend/src/Squidex.Shared/Texts.fr.resx

@ -53,10 +53,10 @@
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="annotations_AbsoluteUrl" xml:space="preserve">
<value>Le champ '{name|lower}' doit être une URL absolue.</value>

4
backend/src/Squidex.Shared/Texts.it.resx

@ -53,10 +53,10 @@
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="annotations_AbsoluteUrl" xml:space="preserve">
<value>Il campo '{name|lower}' deve essere un URL assoluto.</value>

4
backend/src/Squidex.Shared/Texts.nl.resx

@ -53,10 +53,10 @@
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="annotations_AbsoluteUrl" xml:space="preserve">
<value>Het veld '{name|lower}' moet een absolute URL zijn.</value>

4
backend/src/Squidex.Shared/Texts.pt.resx

@ -53,10 +53,10 @@
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="annotations_AbsoluteUrl" xml:space="preserve">
<value>O campo {name|lower} deve ser uma URL absoluta.</value>

4
backend/src/Squidex.Shared/Texts.resx

@ -53,10 +53,10 @@
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="annotations_AbsoluteUrl" xml:space="preserve">
<value>The field '{name|lower}' must be an absolute URL.</value>

4
backend/src/Squidex.Shared/Texts.zh.resx

@ -53,10 +53,10 @@
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="annotations_AbsoluteUrl" xml:space="preserve">
<value>字段 '{name|lower}' 必须是绝对 URL。</value>

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

@ -39,6 +39,16 @@ public sealed class SchemaPropertiesDto
/// </summary>
public string? ContentEditorUrl { get; set; }
/// <summary>
/// The url to the editor plugin.
/// </summary>
public string? ContentsEditorUrl { get; set; }
/// <summary>
/// The url to the content list plugin.
/// </summary>
public string? ContentsListUrl { get; set; }
/// <summary>
/// True to validate the content items on publish.
/// </summary>

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

@ -40,9 +40,9 @@ public sealed class UpdateSchemaDto
public string? ContentSidebarUrl { get; set; }
/// <summary>
/// The url to the editor plugin.
/// The url to the content list plugin.
/// </summary>
public string? ContentEditorUrl { get; set; }
public string? ContentsListUrl { get; set; }
/// <summary>
/// True to validate the content items on publish.

2
backend/src/Squidex/wwwroot/scripts/editor-sdk.js

@ -171,6 +171,8 @@ function SquidexSidebar(options) {
return plugin;
}
var SquidexPlugin = SquidexSidebar;
/**
* Creates a new plugin for widgets.

13
backend/src/Squidex/wwwroot/scripts/sidebar-content.html

@ -12,6 +12,15 @@
body {
background-color: white;
}
div {
padding: 20px;
}
textarea {
overflow-x: hidden;
overflow-y: hidden;
}
</style>
</head>
@ -23,7 +32,9 @@
}
</script>
<textarea class="form-control" oninput="grow(this)" id="editor"></textarea>
<div>
<textarea class="form-control" oninput="grow(this)" id="editor"></textarea>
</div>
<script>
var element = document.getElementById('editor');

6
backend/src/Squidex/wwwroot/scripts/sidebar-context.html

@ -12,6 +12,12 @@
body {
background-color: white;
}
textarea {
overflow-x: hidden;
overflow-y: hidden;
margin: 20px;
}
</style>
</head>

4
frontend/src/app/features/content/pages/content/content-page.component.html

@ -146,9 +146,9 @@
</ng-container>
<ng-container *ngSwitchCase="'extension'">
<sqx-content-extension mode="referencing" *ngIf="schema.properties.contentEditorUrl && content"
[editorUrl]="schema.properties.contentEditorUrl"
[contentItem]="content"
[contentSchema]="schema"
[url]="schema.properties.contentEditorUrl">
[contentSchema]="schema">
</sqx-content-extension>
</ng-container>
</ng-container>

10
frontend/src/app/features/content/pages/contents-plugin/contents-plugin.component.html

@ -0,0 +1,10 @@
<sqx-layout layout="main" titleText="i18n:common.contents" titleIcon="contents" hideHeader="true" hideSidebar="true" white="true" overflow="true">
<ng-container *ngIf="schema | async; let schema">
<sqx-content-extension
[editorUrl]="schema.properties.contentsListUrl"
[contentItem]="undefined"
[contentSchema]="schema"
scrollable="true">
</sqx-content-extension>
</ng-container>
</sqx-layout>

13
frontend/src/app/features/content/pages/contents-plugin/contents-plugin.component.scss

@ -0,0 +1,13 @@
@import 'mixins';
@import 'vars';
:host ::ng-deep {
.panel2-main-inner {
position: relative;
}
iframe {
@include absolute(0, 0, 0, 0);
height: 100% !important
}
}

33
frontend/src/app/features/content/pages/contents-plugin/contents-plugin.component.ts

@ -0,0 +1,33 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { AsyncPipe, NgIf } from '@angular/common';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { LayoutComponent, SchemasState } from '@app/shared';
import { ContentExtensionComponent } from '../../shared/content-extension.component';
@Component({
standalone: true,
selector: 'sqx-contents-plugin',
styleUrls: ['./contents-plugin.component.scss'],
templateUrl: './contents-plugin.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
AsyncPipe,
ContentExtensionComponent,
LayoutComponent,
NgIf,
],
})
export class ContentsPluginComponent {
public schema = this.schemasState.selectedSchema;
constructor(
private readonly schemasState: SchemasState,
) {
}
}

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

@ -1,8 +1,9 @@
<sqx-layout titleText="i18n:common.sidebar" width="20" white="true" padding="true" overflow="true">
<sqx-layout layout="right" titleText="i18n:common.sidebar" width="20" white="true">
<sqx-content-extension
[url]="url | async"
[editorUrl]="url | async"
[contentItem]="contentsState.selectedContent | async"
[contentSchema]="(schemasState.selectedSchema | async)!">
[contentSchema]="(schemasState.selectedSchema | async)!"
scrollable="true">
</sqx-content-extension>
</sqx-layout>

13
frontend/src/app/features/content/pages/sidebar/sidebar-page.component.scss

@ -1,8 +1,13 @@
@import 'mixins';
@import 'vars';
iframe {
background: 0;
border: 0;
overflow: hidden;
:host ::ng-deep {
.panel2-main-inner {
position: relative;
}
iframe {
@include absolute(0, 0, 0, 0);
height: 100% !important
}
}

8
frontend/src/app/features/content/routes.ts

@ -13,6 +13,7 @@ import { ContentHistoryPageComponent } from './pages/content/content-history-pag
import { ContentPageComponent } from './pages/content/content-page.component';
import { ContentsFiltersPageComponent } from './pages/contents/contents-filters-page.component';
import { ContentsPageComponent } from './pages/contents/contents-page.component';
import { ContentsPluginComponent } from './pages/contents-plugin/contents-plugin.component';
import { ReferencesPageComponent } from './pages/references/references-page.component';
import { SchemasPageComponent } from './pages/schemas/schemas-page.component';
import { SidebarPageComponent } from './pages/sidebar/sidebar-page.component';
@ -38,7 +39,7 @@ export const CONTENT_ROUTES: Routes = [
{
path: '',
component: ContentsPageComponent,
canActivate: [schemaMustNotBeSingletonGuard, contentMustExistGuard],
canActivate: [schemaMustNotBeSingletonGuard(false), contentMustExistGuard],
canDeactivate: [canDeactivateGuard],
children: [
{
@ -51,6 +52,11 @@ export const CONTENT_ROUTES: Routes = [
},
],
},
{
path: 'extension',
canActivate: [schemaMustNotBeSingletonGuard(true), contentMustExistGuard],
component: ContentsPluginComponent,
},
{
path: 'new',
component: ContentPageComponent,

2
frontend/src/app/features/content/shared/content-extension.component.html

@ -1 +1 @@
<iframe #iframe scrolling="no" width="100%" [attr.src]="computedUrl | sqxSafeResourceUrl"></iframe>
<iframe #iframe [attr.scrollable]="scrollable ? 'yes' : 'no'" width="100%" [attr.src]="computedUrl | sqxSafeResourceUrl"></iframe>

9
frontend/src/app/features/content/shared/content-extension.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, ElementRef, HostListener, Input, ViewChild } from '@angular/core';
import { booleanAttribute, ChangeDetectionStrategy, Component, ElementRef, HostListener, Input, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { ApiUrlConfig, AppsState, AuthService, computeEditorUrl, ContentDto, SafeResourceUrlPipe, SchemaDto, TypedSimpleChanges, Types } from '@app/shared';
@ -32,8 +32,11 @@ export class ContentExtensionComponent {
@Input({ required: true })
public contentSchema!: SchemaDto;
@Input({ transform: booleanAttribute })
public scrollable?: boolean = false;
@Input()
public set url(value: string | undefined | null) {
public set editorUrl(value: string | undefined | null) {
this.computedUrl = computeEditorUrl(value, this.appsState.snapshot.selectedSettings);
}
@ -72,7 +75,7 @@ export class ContentExtensionComponent {
this.sendInit();
this.sendContent();
} else if (type === 'resize') {
} else if (type === 'resize' && !this.scrollable) {
const { height } = event.data;
this.iframe.nativeElement.height = `${height}px`;

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

@ -57,7 +57,17 @@
<input type="url" class="form-control" id="contentEditorUrl" formControlName="contentEditorUrl">
<sqx-form-hint>{{ 'schemas.contentEditorUrl' | sqxTranslate }}</sqx-form-hint>
<sqx-form-hint>{{ 'schemas.contentEditorUrlHint' | sqxTranslate }}</sqx-form-hint>
</div>
<div class="form-group">
<label for="hints">{{ 'schemas.contentsListUrl' | sqxTranslate }}</label>
<sqx-control-errors for="contentsListUrl"></sqx-control-errors>
<input type="url" class="form-control" id="contentsListUrl" formControlName="contentsListUrl">
<sqx-form-hint>{{ 'schemas.contentsListUrlHint' | sqxTranslate }}</sqx-form-hint>
</div>
<div class="form-group">

57
frontend/src/app/shared/guards/schema-must-not-be-singleton.guard.spec.ts

@ -19,7 +19,7 @@ describe('SchemaMustNotBeSingletonGuard', () => {
},
url: [
new UrlSegment('schemas', {}),
new UrlSegment('name', {}),
new UrlSegment('my-schema', {}),
new UrlSegment('new', {}),
],
};
@ -46,12 +46,12 @@ describe('SchemaMustNotBeSingletonGuard', () => {
});
bit('should subscribe to schema and return true if default', async () => {
const state: RouterStateSnapshot = <any>{ url: 'schemas/name/' };
const state: RouterStateSnapshot = <any>{ url: 'schemas/my-schema/' };
schemasState.setup(x => x.selectedSchema)
.returns(() => of(<SchemaDto>{ id: '123', type: 'Default' }));
.returns(() => of(<SchemaDto>{ id: '123', type: 'Default', properties: {} }));
const result = await firstValueFrom(schemaMustNotBeSingletonGuard(route, state));
const result = await firstValueFrom(schemaMustNotBeSingletonGuard(false)(route, state));
expect(result).toBeTruthy();
@ -59,12 +59,12 @@ describe('SchemaMustNotBeSingletonGuard', () => {
});
bit('should redirect to content if singleton', async () => {
const state: RouterStateSnapshot = <any>{ url: 'schemas/name/' };
const state: RouterStateSnapshot = <any>{ url: 'schemas/my-schema/' };
schemasState.setup(x => x.selectedSchema)
.returns(() => of(<SchemaDto>{ id: '123', type: 'Singleton' }));
const result = await firstValueFrom(schemaMustNotBeSingletonGuard(route, state));
const result = await firstValueFrom(schemaMustNotBeSingletonGuard(false)(route, state));
expect(result).toBeFalsy();
@ -72,16 +72,55 @@ describe('SchemaMustNotBeSingletonGuard', () => {
});
bit('should redirect to content if singleton on new page', async () => {
const state: RouterStateSnapshot = <any>{ url: 'schemas/name/new/' };
const state: RouterStateSnapshot = <any>{ url: 'schemas/my-schema/new/' };
schemasState.setup(x => x.selectedSchema)
.returns(() => of(<SchemaDto>{ id: '123', type: 'Singleton' }));
const result = await firstValueFrom(schemaMustNotBeSingletonGuard(route, state));
const result = await firstValueFrom(schemaMustNotBeSingletonGuard(false)(route, state));
expect(result).toBeFalsy();
router.verify(x => x.navigate(['schemas/name/', '123']), Times.once());
router.verify(x => x.navigate(['schemas/my-schema/', '123']), Times.once());
});
bit('should redirect to extension if list url is configured', async () => {
const state: RouterStateSnapshot = <any>{ url: 'schemas/my-schema/' };
schemasState.setup(x => x.selectedSchema)
.returns(() => of(<SchemaDto>{ id: '123', type: 'Default', properties: { contentsListUrl: 'extension' } }));
const result = await firstValueFrom(schemaMustNotBeSingletonGuard(false)(route, state));
expect(result).toBeFalsy();
router.verify(x => x.navigate([state.url, 'extension']), Times.once());
});
bit('should redirect to extension if list url is not configured on extension page', async () => {
const state: RouterStateSnapshot = <any>{ url: 'schemas/my-schema/' };
schemasState.setup(x => x.selectedSchema)
.returns(() => of(<SchemaDto>{ id: '123', type: 'Default', properties: {} }));
const result = await firstValueFrom(schemaMustNotBeSingletonGuard(true)(route, state));
expect(result).toBeFalsy();
router.verify(x => x.navigate([state.url, '..']), Times.once());
});
bit('should return true when schemas has extension on extension page', async () => {
const state: RouterStateSnapshot = <any>{ url: 'schemas/my-schema/' };
schemasState.setup(x => x.selectedSchema)
.returns(() => of(<SchemaDto>{ id: '123', type: 'Default', properties: { contentsListUrl: 'extension' } }));
const result = await firstValueFrom(schemaMustNotBeSingletonGuard(true)(route, state));
expect(result).toBeTruthy();
router.verify(x => x.navigate(It.isAny()), Times.never());
});
});

44
frontend/src/app/shared/guards/schema-must-not-be-singleton.guard.ts

@ -11,26 +11,34 @@ import { map, take, tap } from 'rxjs/operators';
import { defined } from '@app/framework';
import { SchemasState } from '../state/schemas.state';
export const schemaMustNotBeSingletonGuard = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
const schemasState = inject(SchemasState);
const router = inject(Router);
export const schemaMustNotBeSingletonGuard = (forExtension: boolean) => {
return (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
const schemasState = inject(SchemasState);
const router = inject(Router);
const result =
schemasState.selectedSchema.pipe(
defined(),
take(1),
tap(schema => {
if (schema.type === 'Singleton') {
if (state.url.includes('/new')) {
const parentUrl = state.url.slice(0, state.url.indexOf(route.url[route.url.length - 1].path));
const result =
schemasState.selectedSchema.pipe(
defined(),
take(1),
tap(schema => {
if (schema.type === 'Singleton') {
if (state.url.includes('/new')) {
const parentUrl = state.url.slice(0, state.url.indexOf(route.url[route.url.length - 1].path));
router.navigate([parentUrl, schema.id]);
} else {
router.navigate([state.url, schema.id]);
router.navigate([parentUrl, schema.id]);
} else {
router.navigate([state.url, schema.id]);
}
} else if (!!schema.properties.contentsListUrl !== forExtension) {
if (forExtension) {
router.navigate([state.url, '..']);
} else {
router.navigate([state.url, 'extension']);
}
}
}
}),
map(schema => schema.type === 'Default'));
}),
map(schema => schema.type === 'Default' && !!schema.properties.contentsListUrl === forExtension));
return result;
return result;
};
};

2
frontend/src/app/shared/services/schemas.service.spec.ts

@ -654,6 +654,7 @@ describe('SchemasService', () => {
contentsSidebarUrl: `url/to/contents/${key}`,
contentSidebarUrl: `url/to/content/${key}`,
contentEditorUrl: `url/to/editor/${key}`,
contentsListUrl: `url/to/list/${key}`,
tags: [
`tags${key}`,
],
@ -879,6 +880,7 @@ function createSchemaProperties(id: number, suffix = '') {
`url/to/contents/${key}`,
`url/to/content/${key}`,
`url/to/editor/${key}`,
`url/to/list/${key}`,
id % 2 === 1,
[
`tags${key}`,

5
frontend/src/app/shared/services/schemas.service.ts

@ -380,6 +380,7 @@ export class SchemaPropertiesDto {
public readonly contentsSidebarUrl?: string,
public readonly contentSidebarUrl?: string,
public readonly contentEditorUrl?: string,
public readonly contentsListUrl?: string,
public readonly validateOnPublish?: boolean,
public readonly tags?: ReadonlyArray<string>,
) {
@ -495,6 +496,9 @@ export type UpdateSchemaDto = Readonly<{
// The URL to an editor to replace the editor.
contentEditorUrl?: string;
// The url to the content list plugin.
contentsListUrl?: string;
// True, if the content should be validated on publishing.
validateOnPublish?: boolean;
@ -835,6 +839,7 @@ function parseProperties(response: any) {
response.contentsSidebarUrl,
response.contentSidebarUrl,
response.contentEditorUrl,
response.contentsListUrl,
response.validateOnPublish,
response.tags);
}

3
frontend/src/app/shared/state/schemas.forms.ts

@ -389,6 +389,9 @@ export class EditSchemaForm extends Form<ExtendedFormGroup, UpdateSchemaDto, Sch
contentEditorUrl: new UntypedFormControl('',
Validators.nullValidator,
),
contentsListUrl: new UntypedFormControl('',
Validators.nullValidator,
),
validateOnPublish: new UntypedFormControl(false,
Validators.nullValidator,
),

Loading…
Cancel
Save