Browse Source

Feature/copy language (#770)

* Refactor content-page actions.

* Copy drop down.

* File was not saved.

* Getting rid of toolbar logic from content-page.

* Better reuse of components.

* Fix tests
pull/772/head
Sebastian Stehle 4 years ago
committed by GitHub
parent
commit
2734b8d072
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      backend/i18n/frontend_en.json
  2. 3
      backend/i18n/frontend_it.json
  3. 3
      backend/i18n/frontend_nl.json
  4. 3
      backend/i18n/frontend_zh.json
  5. 3
      backend/i18n/source/frontend_en.json
  6. 83
      backend/src/Squidex/wwwroot/scripts/editor-references.html
  7. 6
      backend/src/Squidex/wwwroot/scripts/editor-sdk.js
  8. 9
      frontend/app/app.module.ts
  9. 1
      frontend/app/features/content/declarations.ts
  10. 9
      frontend/app/features/content/module.ts
  11. 54
      frontend/app/features/content/pages/content/content-page.component.html
  12. 32
      frontend/app/features/content/pages/content/content-page.component.ts
  13. 2
      frontend/app/features/content/pages/content/editor/content-editor.component.html
  14. 8
      frontend/app/features/content/pages/content/editor/content-editor.component.scss
  15. 6
      frontend/app/features/content/pages/content/editor/content-field.component.html
  16. 2
      frontend/app/features/content/pages/content/editor/content-field.component.scss
  17. 8
      frontend/app/features/content/pages/content/editor/content-field.component.ts
  18. 37
      frontend/app/features/content/pages/content/editor/field-copy-button.component.html
  19. 3
      frontend/app/features/content/pages/content/editor/field-copy-button.component.scss
  20. 61
      frontend/app/features/content/pages/content/editor/field-copy-button.component.ts
  21. 2
      frontend/app/features/content/pages/content/inspecting/content-inspection.component.html
  22. 6
      frontend/app/features/content/pages/content/inspecting/content-inspection.component.scss
  23. 36
      frontend/app/features/content/pages/content/inspecting/content-inspection.component.ts
  24. 30
      frontend/app/features/content/pages/content/references/content-references.component.ts
  25. 2
      frontend/app/features/content/shared/forms/iframe-editor.component.html
  26. 6
      frontend/app/framework/angular/forms/editors/checkbox-group.component.html
  27. 20
      frontend/app/framework/angular/forms/editors/checkbox-group.component.ts
  28. 5
      frontend/app/framework/angular/toolbar.component.html
  29. 0
      frontend/app/framework/angular/toolbar.component.scss
  30. 22
      frontend/app/framework/angular/toolbar.component.ts
  31. 1
      frontend/app/framework/declarations.ts
  32. 3
      frontend/app/framework/internal.ts
  33. 4
      frontend/app/framework/module.ts
  34. 113
      frontend/app/framework/services/toolbar.service.spec.ts
  35. 55
      frontend/app/framework/services/toolbar.service.ts
  36. 12
      frontend/app/shared/state/contents.forms-helpers.ts
  37. 21
      frontend/app/shared/state/contents.forms.ts
  38. 1
      frontend/app/theme/_forms.scss

3
backend/i18n/frontend_en.json

@ -239,6 +239,7 @@
"common.contents": "Contents", "common.contents": "Contents",
"common.continue": "Continue", "common.continue": "Continue",
"common.contributors": "Contributors", "common.contributors": "Contributors",
"common.copy": "Copy",
"common.create": "Create", "common.create": "Create",
"common.created": "Created", "common.created": "Created",
"common.daily": "Daily", "common.daily": "Daily",
@ -278,6 +279,7 @@
"common.filters": "Filters", "common.filters": "Filters",
"common.folder": "Folder", "common.folder": "Folder",
"common.folders": "Folders", "common.folders": "Folders",
"common.from": "From",
"common.generalSettings": "Common", "common.generalSettings": "Common",
"common.generate": "Generate", "common.generate": "Generate",
"common.github": "Github", "common.github": "Github",
@ -367,6 +369,7 @@
"common.tags": "Tags", "common.tags": "Tags",
"common.tagsAll": "All tags", "common.tagsAll": "All tags",
"common.time": "Time", "common.time": "Time",
"common.to": "To",
"common.update": "Update", "common.update": "Update",
"common.upload": "Upload", "common.upload": "Upload",
"common.url": "URL", "common.url": "URL",

3
backend/i18n/frontend_it.json

@ -239,6 +239,7 @@
"common.contents": "Contenuti", "common.contents": "Contenuti",
"common.continue": "Continua", "common.continue": "Continua",
"common.contributors": "Collaboratori", "common.contributors": "Collaboratori",
"common.copy": "Copy",
"common.create": "Crea", "common.create": "Crea",
"common.created": "Creato", "common.created": "Creato",
"common.daily": "Daily", "common.daily": "Daily",
@ -278,6 +279,7 @@
"common.filters": "Filtri", "common.filters": "Filtri",
"common.folder": "Cartella", "common.folder": "Cartella",
"common.folders": "Cartelle", "common.folders": "Cartelle",
"common.from": "From",
"common.generalSettings": "Impostazioni generali", "common.generalSettings": "Impostazioni generali",
"common.generate": "Genera", "common.generate": "Genera",
"common.github": "Github", "common.github": "Github",
@ -367,6 +369,7 @@
"common.tags": "Tag", "common.tags": "Tag",
"common.tagsAll": "Tutti i tag", "common.tagsAll": "Tutti i tag",
"common.time": "Ora", "common.time": "Ora",
"common.to": "To",
"common.update": "Aggiorna", "common.update": "Aggiorna",
"common.upload": "Carica", "common.upload": "Carica",
"common.url": "URL", "common.url": "URL",

3
backend/i18n/frontend_nl.json

@ -239,6 +239,7 @@
"common.contents": "Inhoud", "common.contents": "Inhoud",
"common.continue": "Doorgaan", "common.continue": "Doorgaan",
"common.contributors": "Bijdragers", "common.contributors": "Bijdragers",
"common.copy": "Copy",
"common.create": "Maken", "common.create": "Maken",
"common.created": "Gemaakt", "common.created": "Gemaakt",
"common.daily": "Daily", "common.daily": "Daily",
@ -278,6 +279,7 @@
"common.filters": "Filters", "common.filters": "Filters",
"common.folder": "Folder", "common.folder": "Folder",
"common.folders": "Mappen", "common.folders": "Mappen",
"common.from": "From",
"common.generalSettings": "Algemeen", "common.generalSettings": "Algemeen",
"common.generate": "Genereren", "common.generate": "Genereren",
"common.github": "Github", "common.github": "Github",
@ -367,6 +369,7 @@
"common.tags": "Tags", "common.tags": "Tags",
"common.tagsAll": "Alle tags", "common.tagsAll": "Alle tags",
"common.time": "Tijd", "common.time": "Tijd",
"common.to": "To",
"common.update": "Update", "common.update": "Update",
"common.upload": "Upload", "common.upload": "Upload",
"common.url": "URL", "common.url": "URL",

3
backend/i18n/frontend_zh.json

@ -239,6 +239,7 @@
"common.contents": "内容", "common.contents": "内容",
"common.continue": "继续", "common.continue": "继续",
"common.contributors": "贡献者", "common.contributors": "贡献者",
"common.copy": "Copy",
"common.create": "创建", "common.create": "创建",
"common.created": "创建", "common.created": "创建",
"common.daily": "Daily", "common.daily": "Daily",
@ -278,6 +279,7 @@
"common.filters": "过滤器", "common.filters": "过滤器",
"common.folder": "文件夹", "common.folder": "文件夹",
"common.folders": "文件夹", "common.folders": "文件夹",
"common.from": "From",
"common.generalSettings": "通用", "common.generalSettings": "通用",
"common.generate": "生成", "common.generate": "生成",
"common.github": "Github", "common.github": "Github",
@ -367,6 +369,7 @@
"common.tags": "标签", "common.tags": "标签",
"common.tagsAll": "所有标签", "common.tagsAll": "所有标签",
"common.time": "时间", "common.time": "时间",
"common.to": "To",
"common.update": "更新", "common.update": "更新",
"common.upload": "上传", "common.upload": "上传",
"common.url": "URL", "common.url": "URL",

3
backend/i18n/source/frontend_en.json

@ -239,6 +239,7 @@
"common.contents": "Contents", "common.contents": "Contents",
"common.continue": "Continue", "common.continue": "Continue",
"common.contributors": "Contributors", "common.contributors": "Contributors",
"common.copy": "Copy",
"common.create": "Create", "common.create": "Create",
"common.created": "Created", "common.created": "Created",
"common.daily": "Daily", "common.daily": "Daily",
@ -278,6 +279,7 @@
"common.filters": "Filters", "common.filters": "Filters",
"common.folder": "Folder", "common.folder": "Folder",
"common.folders": "Folders", "common.folders": "Folders",
"common.from": "From",
"common.generalSettings": "Common", "common.generalSettings": "Common",
"common.generate": "Generate", "common.generate": "Generate",
"common.github": "Github", "common.github": "Github",
@ -367,6 +369,7 @@
"common.tags": "Tags", "common.tags": "Tags",
"common.tagsAll": "All tags", "common.tagsAll": "All tags",
"common.time": "Time", "common.time": "Time",
"common.to": "To",
"common.update": "Update", "common.update": "Update",
"common.upload": "Upload", "common.upload": "Upload",
"common.url": "URL", "common.url": "URL",

83
backend/src/Squidex/wwwroot/scripts/editor-references.html

@ -0,0 +1,83 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js -->
<script src="editor-sdk.js"></script>
<link rel="stylesheet" type="text/css" href="https://cloud.squidex.io/build/app.css">
<style>
body {
height: 38px;
}
</style>
</head>
<body>
<select class="form-select" id="editor">
<option></option>
</textarea>
<script>
const element = document.getElementById('editor');
// When the field is instantiated it notifies the UI that it has been loaded.
//
// Furthermore it sends the current size to the parent.
const field = new SquidexFormField();
field.onInit(context => {
// Fetch the references with a custom filter.
fetch(`${context.apiUrl}/content/${context.appName}/references?filter=data/isActive/iv eq true`, {
headers: {
Authorization: `Bearer ${context.user.user.access_token}`
}
})
.then(x => x.json())
.then(x => {
for (var item of x.items) {
// Use the title field as option text.
element.add(new Option(item.data.title.iv, item.id));
}
// Update the value again for the new options.
updateValue(field.getValue());
})
});
// Handle the value change event and set the text to the editor.
field.onValueChanged(function (value) {
updateValue(value);
});
// Disable the editor when it should be disabled.
field.onDisabled(function (disabled) {
updateDisabled(disabled);
});
element.addEventListener('change', event => {
if (element.value) {
field.valueChanged([element.value]);
} else {
field.valueChanged([]);
}
});
function updateDisabled(disabled) {
element.disabled = disabled;
}
function updateValue(value) {
if (Array.isArray(value) && value.length > 0) {
element.value = value[0];
} else {
element.value = undefined;
}
}
</script>
</body>
</html>

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

@ -454,14 +454,14 @@ function SquidexFormField() {
* *
* @param {Function} callback: The callback to invoke. Argument 1: New position (number). * @param {Function} callback: The callback to invoke. Argument 1: New position (number).
*/ */
onInit: function (callback) { onMoved: function (callback) {
if (!isFunction(callback)) { if (!isFunction(callback)) {
return; return;
} }
initHandler = callback; movedHandler = callback;
raiseInit(); raisedMoved();
}, },
/** /**

9
frontend/app/app.module.ts

@ -14,7 +14,7 @@ import { ApplicationRef, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule } from '@angular/router'; import { ActivatedRouteSnapshot, BaseRouteReuseStrategy, RouteReuseStrategy, RouterModule } from '@angular/router';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { routing } from './app.routes'; import { routing } from './app.routes';
import { ApiUrlConfig, CurrencyConfig, DateHelper, DecimalSeparatorConfig, LocalizerService, SqxFrameworkModule, SqxSharedModule, TitlesConfig, UIOptions } from './shared'; import { ApiUrlConfig, CurrencyConfig, DateHelper, DecimalSeparatorConfig, LocalizerService, SqxFrameworkModule, SqxSharedModule, TitlesConfig, UIOptions } from './shared';
@ -68,6 +68,12 @@ function configLocalizerService() {
} }
} }
export class AppRouteReuseStrategy extends BaseRouteReuseStrategy {
public shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot) {
return (future.routeConfig === curr.routeConfig) || (future.data.reuseId && future.data.reuseId === curr.data.reuseId);
}
}
@NgModule({ @NgModule({
imports: [ imports: [
BrowserAnimationsModule, BrowserAnimationsModule,
@ -90,6 +96,7 @@ function configLocalizerService() {
{ provide: CurrencyConfig, useFactory: configCurrency }, { provide: CurrencyConfig, useFactory: configCurrency },
{ provide: DecimalSeparatorConfig, useFactory: configDecimalSeparator }, { provide: DecimalSeparatorConfig, useFactory: configDecimalSeparator },
{ provide: LocalizerService, useFactory: configLocalizerService }, { provide: LocalizerService, useFactory: configLocalizerService },
{ provide: RouteReuseStrategy, useClass: AppRouteReuseStrategy },
{ provide: TitlesConfig, useFactory: configTitles }, { provide: TitlesConfig, useFactory: configTitles },
{ provide: UIOptions, useFactory: configUIOptions }, { provide: UIOptions, useFactory: configUIOptions },
], ],

1
frontend/app/features/content/declarations.ts

@ -13,6 +13,7 @@ export * from './pages/content/content-page.component';
export * from './pages/content/editor/content-editor.component'; export * from './pages/content/editor/content-editor.component';
export * from './pages/content/editor/content-field.component'; export * from './pages/content/editor/content-field.component';
export * from './pages/content/editor/content-section.component'; export * from './pages/content/editor/content-section.component';
export * from './pages/content/editor/field-copy-button.component';
export * from './pages/content/editor/field-languages.component'; export * from './pages/content/editor/field-languages.component';
export * from './pages/content/inspecting/content-inspection.component'; export * from './pages/content/inspecting/content-inspection.component';
export * from './pages/content/references/content-references.component'; export * from './pages/content/references/content-references.component';

9
frontend/app/features/content/module.ts

@ -10,7 +10,7 @@ import { RouterModule, Routes } from '@angular/router';
import { CanDeactivateGuard, ContentMustExistGuard, LoadLanguagesGuard, LoadSchemasGuard, SchemaMustExistPublishedGuard, SchemaMustNotBeSingletonGuard, SqxFrameworkModule, SqxSharedModule } from '@app/shared'; import { CanDeactivateGuard, ContentMustExistGuard, LoadLanguagesGuard, LoadSchemasGuard, SchemaMustExistPublishedGuard, SchemaMustNotBeSingletonGuard, SqxFrameworkModule, SqxSharedModule } from '@app/shared';
import { ScrollingModule } from '@angular/cdk/scrolling'; import { ScrollingModule } from '@angular/cdk/scrolling';
import { ScrollingModule as ScrollingModuleExperimental } from '@angular/cdk-experimental/scrolling'; import { ScrollingModule as ScrollingModuleExperimental } from '@angular/cdk-experimental/scrolling';
import { ArrayEditorComponent, ArrayItemComponent, AssetsEditorComponent, CalendarPageComponent, CommentsPageComponent, ComponentComponent, ComponentSectionComponent, ContentComponent, ContentCreatorComponent, ContentEditorComponent, ContentEventComponent, ContentExtensionComponent, ContentFieldComponent, ContentHistoryPageComponent, ContentInspectionComponent, ContentPageComponent, ContentReferencesComponent, ContentSectionComponent, ContentsFiltersPageComponent, ContentsPageComponent, CustomViewEditorComponent, DueTimeSelectorComponent, FieldEditorComponent, FieldLanguagesComponent, IFrameEditorComponent, PreviewButtonComponent, ReferenceItemComponent, ReferencesEditorComponent, SchemasPageComponent, SidebarPageComponent, StockPhotoEditorComponent } from './declarations'; import { ArrayEditorComponent, ArrayItemComponent, AssetsEditorComponent, CalendarPageComponent, CommentsPageComponent, ComponentComponent, ComponentSectionComponent, ContentComponent, ContentCreatorComponent, ContentEditorComponent, ContentEventComponent, ContentExtensionComponent, ContentFieldComponent, ContentHistoryPageComponent, ContentInspectionComponent, ContentPageComponent, ContentReferencesComponent, ContentSectionComponent, ContentsFiltersPageComponent, ContentsPageComponent, CustomViewEditorComponent, DueTimeSelectorComponent, FieldCopyButtonComponent, FieldEditorComponent, FieldLanguagesComponent, IFrameEditorComponent, PreviewButtonComponent, ReferenceItemComponent, ReferencesEditorComponent, SchemasPageComponent, SidebarPageComponent, StockPhotoEditorComponent } from './declarations';
const routes: Routes = [ const routes: Routes = [
{ {
@ -50,12 +50,18 @@ const routes: Routes = [
component: ContentPageComponent, component: ContentPageComponent,
canActivate: [SchemaMustNotBeSingletonGuard, ContentMustExistGuard], canActivate: [SchemaMustNotBeSingletonGuard, ContentMustExistGuard],
canDeactivate: [CanDeactivateGuard], canDeactivate: [CanDeactivateGuard],
data: {
reuseId: 'contentPage',
},
}, },
{ {
path: ':contentId', path: ':contentId',
component: ContentPageComponent, component: ContentPageComponent,
canActivate: [ContentMustExistGuard], canActivate: [ContentMustExistGuard],
canDeactivate: [CanDeactivateGuard], canDeactivate: [CanDeactivateGuard],
data: {
reuseId: 'contentPage',
},
children: [ children: [
{ {
path: 'history', path: 'history',
@ -110,6 +116,7 @@ const routes: Routes = [
ContentsPageComponent, ContentsPageComponent,
CustomViewEditorComponent, CustomViewEditorComponent,
DueTimeSelectorComponent, DueTimeSelectorComponent,
FieldCopyButtonComponent,
FieldEditorComponent, FieldEditorComponent,
FieldLanguagesComponent, FieldLanguagesComponent,
IFrameEditorComponent, IFrameEditorComponent,

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

@ -57,29 +57,6 @@
<sqx-language-selector class="languages-buttons" [(language)]="language" [languages]="languages"></sqx-language-selector> <sqx-language-selector class="languages-buttons" [(language)]="language" [languages]="languages"></sqx-language-selector>
</ng-container> </ng-container>
<ng-container *ngIf="contentTab | async; let tab">
<ng-container *ngIf="tab === 'references' || tab === 'referencing'; else defaultHeader">
<ng-container *ngIf="content?.canDelete">
<button type="button" class="btn btn-outline-secondary ms-2" (click)="dropdown.toggle()" #buttonOptions>
<i class="icon-dots"></i>
</button>
<ng-container *sqxModal="dropdown;closeAlways:true">
<div class="dropdown-menu" [sqxAnchoredTo]="buttonOptions" @fade>
<a class="dropdown-item" (click)="publish()">
{{ 'contents.publishAll' | sqxTranslate }}
</a>
<a class="dropdown-item" (click)="validate()">
{{ 'contents.validate' | sqxTranslate }}
</a>
</div>
</ng-container>
</ng-container>
</ng-container>
<ng-template #defaultHeader>
<sqx-preview-button [schema]="schema" [content]="content" [confirm]="confirmPreview"></sqx-preview-button>
<ng-container *ngIf="content?.canDelete"> <ng-container *ngIf="content?.canDelete">
<button type="button" class="btn btn-outline-secondary ms-2" (click)="dropdown.toggle()" #buttonOptions> <button type="button" class="btn btn-outline-secondary ms-2" (click)="dropdown.toggle()" #buttonOptions>
<i class="icon-dots"></i> <i class="icon-dots"></i>
@ -98,12 +75,18 @@
</ng-container> </ng-container>
</ng-container> </ng-container>
<ng-container *ngIf="content?.canUpdate && (!inspection || inspection.mode.value === 'Data')"> <ng-container *ngIf="contentTab | async; let tab">
<sqx-toolbar></sqx-toolbar>
<ng-container *ngIf="tab === 'editor'">
<sqx-preview-button [schema]="schema" [content]="content" [confirm]="confirmPreview"></sqx-preview-button>
<ng-container *ngIf="content?.canUpdate">
<button type="submit" class="btn btn-primary ms-2" shortcut="CTRL + SHIFT + S"> <button type="submit" class="btn btn-primary ms-2" shortcut="CTRL + SHIFT + S">
{{ 'common.save' | sqxTranslate }} {{ 'common.save' | sqxTranslate }}
</button> </button>
</ng-container> </ng-container>
</ng-template> </ng-container>
</ng-container> </ng-container>
</ng-container> </ng-container>
@ -116,13 +99,11 @@
{{ 'contents.saveAndPublish' | sqxTranslate }} {{ 'contents.saveAndPublish' | sqxTranslate }}
</button> </button>
</ng-template> </ng-template>
<sqx-form-error [bubble]="true" [closeable]="true" [error]="inspection?.contentError || (contentForm.error | async)"></sqx-form-error>
</div> </div>
</ng-container> </ng-container>
<ng-container> <ng-container>
<ng-container *ngIf="content; else noContentEditor"> <ng-container *ngIf="content">
<ng-container [ngSwitch]="contentTab | async"> <ng-container [ngSwitch]="contentTab | async">
<ng-container *ngSwitchCase="'references'"> <ng-container *ngSwitchCase="'references'">
<sqx-content-references mode="references" <sqx-content-references mode="references"
@ -153,23 +134,12 @@
[url]="schema.properties.contentEditorUrl"> [url]="schema.properties.contentEditorUrl">
</sqx-content-extension> </sqx-content-extension>
</ng-container> </ng-container>
<ng-container *ngSwitchDefault>
<sqx-content-editor
[(language)]="language"
[formContext]="formContext"
[contentId]="contentId"
[contentForm]="contentForm"
[contentFormCompare]="contentFormCompare"
[contentVersion]="contentVersion"
[languages]="languages"
[schema]="schema">
</sqx-content-editor>
</ng-container>
</ng-container> </ng-container>
</ng-container> </ng-container>
<ng-template #noContentEditor> <ng-container *ngIf="!content || (contentTab | async) === 'editor'">
<sqx-content-editor <sqx-content-editor
(loadLatest)="loadLatest()"
[(language)]="language" [(language)]="language"
[isNew]="!content" [isNew]="!content"
[contentForm]="contentForm" [contentForm]="contentForm"
@ -180,7 +150,7 @@
[schema]="schema" [schema]="schema"
[(contentId)]="contentId"> [(contentId)]="contentId">
</sqx-content-editor> </sqx-content-editor>
</ng-template> </ng-container>
</ng-container> </ng-container>
<ng-container sidebarMenu> <ng-container sidebarMenu>

32
frontend/app/features/content/pages/content/content-page.component.ts

@ -5,13 +5,11 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, OnInit, ViewChild } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { ApiUrlConfig, AppLanguageDto, AppsState, AuthService, AutoSaveKey, AutoSaveService, CanComponentDeactivate, ContentDto, ContentsState, defined, DialogService, EditContentForm, fadeAnimation, LanguagesState, ModalModel, ResourceOwner, SchemaDto, SchemasState, TempService, Types, Version } from '@app/shared'; import { ApiUrlConfig, AppLanguageDto, AppsState, AuthService, AutoSaveKey, AutoSaveService, CanComponentDeactivate, ContentDto, ContentsState, defined, DialogService, EditContentForm, fadeAnimation, LanguagesState, ModalModel, ResourceOwner, SchemaDto, SchemasState, TempService, ToolbarService, Types, Version } from '@app/shared';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { filter, map, tap } from 'rxjs/operators'; import { filter, map, tap } from 'rxjs/operators';
import { ContentInspectionComponent } from './inspecting/content-inspection.component';
import { ContentReferencesComponent } from './references/content-references.component';
@Component({ @Component({
selector: 'sqx-content-page', selector: 'sqx-content-page',
@ -20,16 +18,13 @@ import { ContentReferencesComponent } from './references/content-references.comp
animations: [ animations: [
fadeAnimation, fadeAnimation,
], ],
providers: [
ToolbarService,
],
}) })
export class ContentPageComponent extends ResourceOwner implements CanComponentDeactivate, OnInit { export class ContentPageComponent extends ResourceOwner implements CanComponentDeactivate, OnInit {
private autoSaveKey: AutoSaveKey; private autoSaveKey: AutoSaveKey;
@ViewChild(ContentReferencesComponent)
public references: ContentReferencesComponent;
@ViewChild(ContentInspectionComponent)
public inspection: ContentInspectionComponent;
public schema: SchemaDto; public schema: SchemaDto;
public formContext: any; public formContext: any;
@ -149,14 +144,6 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
); );
} }
public validate() {
this.references?.validate();
}
public publish() {
this.references?.publish();
}
public saveAndPublish() { public saveAndPublish() {
this.saveContent(true); this.saveContent(true);
} }
@ -166,11 +153,6 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
} }
private saveContent(publish: boolean) { private saveContent(publish: boolean) {
if (this.inspection) {
this.inspection.save();
return;
}
const value = this.contentForm.submit(); const value = this.contentForm.submit();
if (value) { if (value) {
@ -232,10 +214,6 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
} }
} }
public setContentId(id: string) {
this.contentId = id;
}
public checkPendingChangesBeforePreview() { public checkPendingChangesBeforePreview() {
return this.checkPendingChanges('i18n:contents.pendingChangesTextToPreview'); return this.checkPendingChanges('i18n:contents.pendingChangesTextToPreview');
} }

2
frontend/app/features/content/pages/content/editor/content-editor.component.html

@ -1,3 +1,5 @@
<sqx-form-error [bubble]="true" [closeable]="true" [error]="(contentForm.error | async)"></sqx-form-error>
<sqx-list-view> <sqx-list-view>
<ng-container topHeader> <ng-container topHeader>
<div class="alert alert-danger" *ngIf="contentVersion"> <div class="alert alert-danger" *ngIf="contentVersion">

8
frontend/app/features/content/pages/content/editor/content-editor.component.scss

@ -6,6 +6,14 @@
padding: 0; padding: 0;
} }
:host ::ng-deep {
.form-bubble {
.form-alert {
@include absolute(0, 2rem, auto, auto);
}
}
}
.alert { .alert {
border: 0; border: 0;
border-radius: 0; border-radius: 0;

6
frontend/app/features/content/pages/content/editor/content-field.component.html

@ -3,11 +3,9 @@
<div class="table-items-row table-items-row-summary" [class.field-invalid]="isInvalid | async" *ngIf="!(formModel.hiddenChanges | async)"> <div class="table-items-row table-items-row-summary" [class.field-invalid]="isInvalid | async" *ngIf="!(formModel.hiddenChanges | async)">
<div class="languages-container"> <div class="languages-container">
<div class="languages-buttons"> <div class="languages-buttons">
<ng-container *ngIf="!formModel.field.isDisabled"> <button *ngIf="!formModel.field.isDisabled && isTranslatable" type="button" class="btn btn-text-secondary btn-sm me-1" title="i18n:contents.autotranslate" (click)="translate()" tabindex="-1">
<button *ngIf="isTranslatable" type="button" class="btn btn-text-secondary btn-sm me-1" title="i18n:contents.autotranslate" (click)="translate()">
<i class="icon-translate"></i> <i class="icon-translate"></i>
</button> </button>
</ng-container>
<sqx-field-languages <sqx-field-languages
[field]="formModel.field" [field]="formModel.field"
@ -17,6 +15,8 @@
[showAllControls]="showAllControls" [showAllControls]="showAllControls"
(showAllControlsChange)="changeShowAllControls($event)"> (showAllControlsChange)="changeShowAllControls($event)">
</sqx-field-languages> </sqx-field-languages>
<sqx-field-copy-button [formModel]="formModel" [languages]="languages"></sqx-field-copy-button>
</div> </div>
</div> </div>

2
frontend/app/features/content/pages/content/editor/content-field.component.scss

@ -10,11 +10,13 @@
&-buttons { &-buttons {
@include absolute(-.6rem, 2rem); @include absolute(-.6rem, 2rem);
background: $color-white;
z-index: 1000; z-index: 1000;
} }
&-buttons-compare { &-buttons-compare {
@include absolute(-.6rem, 0); @include absolute(-.6rem, 0);
background: $color-white;
z-index: 1000; z-index: 1000;
} }
} }

8
frontend/app/features/content/pages/content/editor/content-field.component.ts

@ -96,9 +96,13 @@ export class ContentFieldComponent implements OnChanges {
public copy() { public copy() {
if (this.formModel && this.formModelCompare) { if (this.formModel && this.formModelCompare) {
if (this.showAllControls) { if (this.showAllControls) {
this.formModel.copyAllFrom(this.formModelCompare); this.formModel.setValue(this.formModelCompare.getRawValue());
} else { } else {
this.formModel.copyFrom(this.formModelCompare, this.language.iso2Code); const target = this.formModel.get(this.language.iso2Code);
if (target) {
target.setValue(this.formModelCompare.get(this.language.iso2Code)?.getRawValue());
}
} }
} }
} }

37
frontend/app/features/content/pages/content/editor/field-copy-button.component.html

@ -0,0 +1,37 @@
<ng-container *ngIf="isLocalized">
<button type="button" class="btn btn-outline-secondary btn-sm ms-1 dropdown-toggle" title="{{ 'common.copy' | sqxTranslate }}" (click)="dropdown.toggle()" #button tabindex="-1">
<i class="icon-copy"></i>
</button>
<ng-container *sqxModal="dropdown">
<div class="dropdown-menu" [sqxAnchoredTo]="button" @fade>
<div class="section d-flex justify-content-end">
<button type="button" class="btn btn-primary" (click)="copy()" tabindex="-1">
{{ 'common.copy' | sqxTranslate }}
</button>
</div>
<div class="dropdown-divider"></div>
<div class="section row">
<label class="col-auto col-form-label" for="languageSource">{{ 'common.from' | sqxTranslate }}</label>
<div class="col">
<select class="form-select" id="languagesSource"
[ngModel]="copySource"
(ngModelChange)="setCopySource($event)">
<option *ngFor="let language of languages" [ngValue]="language.iso2Code">{{language.iso2Code}}</option>
</select>
</div>
</div>
<div class="dropdown-divider"></div>
<div class="section">
<label>{{ 'common.to' | sqxTranslate }}</label>
<sqx-checkbox-group [(ngModel)]="copyTargets" [values]="languageCodes" layout="Multiline"></sqx-checkbox-group>
</div>
</div>
</ng-container>
</ng-container>

3
frontend/app/features/content/pages/content/editor/field-copy-button.component.scss

@ -0,0 +1,3 @@
.section {
padding: .25rem 1rem;
}

61
frontend/app/features/content/pages/content/editor/field-copy-button.component.ts

@ -0,0 +1,61 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { AppLanguageDto, fadeAnimation, FieldForm, ModalModel } from '@app/shared';
@Component({
selector: 'sqx-field-copy-button[formModel][languages]',
styleUrls: ['./field-copy-button.component.scss'],
templateUrl: './field-copy-button.component.html',
animations: [
fadeAnimation,
],
})
export class FieldCopyButtonComponent implements OnChanges {
@Input()
public formModel: FieldForm;
@Input()
public languages: ReadonlyArray<AppLanguageDto>;
public languageCodes: ReadonlyArray<string>;
public copySource: string;
public copyTargets: ReadonlyArray<string>;
public dropdown = new ModalModel();
public get isLocalized() {
return this.formModel.field.isLocalizable && this.languages.length > 1;
}
public ngOnChanges(changes: SimpleChanges) {
if (changes['languages']) {
this.setCopySource(this.languages[0]?.iso2Code);
}
}
public setCopySource(language: string) {
this.copySource = language;
this.copyTargets = [];
this.languageCodes = this.languages.map(x => x.iso2Code).filter(x => x !== language);
}
public copy() {
if (this.copySource && this.copyTargets?.length > 0) {
const source = this.formModel.get(this.copySource).getRawValue();
for (const target of this.copyTargets) {
if (target !== this.copySource) {
this.formModel.get(target)?.setValue(source);
}
}
}
}
}

2
frontend/app/features/content/pages/content/inspecting/content-inspection.component.html

@ -1,3 +1,5 @@
<sqx-form-error [bubble]="true" [closeable]="true" [error]="contentError"></sqx-form-error>
<div class="inner-menu"> <div class="inner-menu">
<ul class="nav nav-tabs2" *ngIf="mode | async; let currentMode"> <ul class="nav nav-tabs2" *ngIf="mode | async; let currentMode">
<li class="nav-item"> <li class="nav-item">

6
frontend/app/features/content/pages/content/inspecting/content-inspection.component.scss

@ -10,6 +10,12 @@
.editor { .editor {
@include absolute(0, 0, 0, 0); @include absolute(0, 0, 0, 0);
} }
.form-bubble {
.form-alert {
@include absolute(0, 2rem, auto, auto);
}
}
} }
.inner-menu, .inner-menu,

36
frontend/app/features/content/pages/content/inspecting/content-inspection.component.ts

@ -5,8 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
import { AppLanguageDto, ContentDto, ContentsService, ContentsState, ErrorDto } from '@app/shared'; import { AppLanguageDto, ContentDto, ContentsService, ContentsState, ErrorDto, ToolbarService } from '@app/shared';
import { BehaviorSubject, combineLatest, of } from 'rxjs'; import { BehaviorSubject, combineLatest, of } from 'rxjs';
import { filter, map, switchMap } from 'rxjs/operators'; import { filter, map, switchMap } from 'rxjs/operators';
@ -17,7 +17,7 @@ type Mode = 'Content' | 'Data' | 'FlatData';
styleUrls: ['./content-inspection.component.scss'], styleUrls: ['./content-inspection.component.scss'],
templateUrl: './content-inspection.component.html', templateUrl: './content-inspection.component.html',
}) })
export class ContentInspectionComponent implements OnChanges { export class ContentInspectionComponent implements OnChanges, OnDestroy {
private languageChanges$ = new BehaviorSubject<AppLanguageDto | null>(null); private languageChanges$ = new BehaviorSubject<AppLanguageDto | null>(null);
@Input() @Input()
@ -58,27 +58,53 @@ export class ContentInspectionComponent implements OnChanges {
})); }));
constructor( constructor(
private readonly changeDetector: ChangeDetectorRef,
private readonly contentsService: ContentsService, private readonly contentsService: ContentsService,
private readonly contentsState: ContentsState, private readonly contentsState: ContentsState,
private toolbar: ToolbarService,
) { ) {
} }
public ngOnDestroy() {
this.toolbar.remove(this);
}
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: SimpleChanges) {
if (changes['language']) { if (changes['language']) {
this.languageChanges$.next(this.language); this.languageChanges$.next(this.language);
} }
if (changes['content']) {
this.updateActions();
}
}
private updateActions() {
if (this.mode.value === 'Data' && this.content.canUpdate) {
this.toolbar.addButton(this, 'common.save', () => {
this.save();
this.changeDetector.detectChanges();
});
} else {
this.toolbar.remove(this);
}
} }
public setData(data: any) { public setData(data: any) {
this.contentData = data; this.contentData = data;
this.updateActions();
} }
public setMode(mode: Mode) { public setMode(mode: Mode) {
this.mode.next(mode); this.mode.next(mode);
this.updateActions();
} }
public save() { private save() {
if (!this.contentData || this.mode.value !== 'Data') { if (!this.contentData) {
return; return;
} }

30
frontend/app/features/content/pages/content/references/content-references.component.ts

@ -5,8 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
import { AppLanguageDto, ComponentContentsState, ContentDto, QuerySynchronizer, Router2State } from '@app/shared'; import { AppLanguageDto, ComponentContentsState, ContentDto, QuerySynchronizer, Router2State, ToolbarService } from '@app/shared';
@Component({ @Component({
selector: 'sqx-content-references[content][language][languages]', selector: 'sqx-content-references[content][language][languages]',
@ -17,7 +17,7 @@ import { AppLanguageDto, ComponentContentsState, ContentDto, QuerySynchronizer,
Router2State, ComponentContentsState, Router2State, ComponentContentsState,
], ],
}) })
export class ContentReferencesComponent implements OnChanges { export class ContentReferencesComponent implements OnChanges, OnInit, OnDestroy {
@Input() @Input()
public content: ContentDto; public content: ContentDto;
@ -33,9 +33,29 @@ export class ContentReferencesComponent implements OnChanges {
constructor( constructor(
public readonly contentsRoute: Router2State, public readonly contentsRoute: Router2State,
public readonly contentsState: ComponentContentsState, public readonly contentsState: ComponentContentsState,
private readonly changeDetector: ChangeDetectorRef,
private readonly toolbar: ToolbarService,
) { ) {
} }
public ngOnDestroy() {
this.toolbar.remove(this);
}
public ngOnInit() {
this.toolbar.addButton(this, 'contents.validate', () => {
this.validate();
this.changeDetector.detectChanges();
});
this.toolbar.addButton(this, 'contents.publishAll', () => {
this.publishAll();
this.changeDetector.detectChanges();
});
}
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: SimpleChanges) {
if (changes['content'] || changes['mode']) { if (changes['content'] || changes['mode']) {
this.contentsState.schema = { name: this.content.schemaName }; this.contentsState.schema = { name: this.content.schemaName };
@ -56,11 +76,11 @@ export class ContentReferencesComponent implements OnChanges {
} }
} }
public validate() { private validate() {
this.contentsState.validate(this.contentsState.snapshot.contents); this.contentsState.validate(this.contentsState.snapshot.contents);
} }
public publish() { private publishAll() {
this.contentsState.changeManyStatus(this.contentsState.snapshot.contents.filter(x => x.canPublish), 'Published'); this.contentsState.changeManyStatus(this.contentsState.snapshot.contents.filter(x => x.canPublish), 'Published');
} }

2
frontend/app/features/content/shared/forms/iframe-editor.component.html

@ -1,6 +1,6 @@
<div #container> <div #container>
<div #inner [class.fullscreen]="snapshot.isFullscreen"> <div #inner [class.fullscreen]="snapshot.isFullscreen">
<iframe #iframe scrolling="no" width="100%" [attr.src]="computedUrl | sqxSafeResourceUrl"></iframe> <iframe #iframe scrolling="no" width="100%" [style.height]="0" [attr.src]="computedUrl | sqxSafeResourceUrl"></iframe>
</div> </div>
</div> </div>

6
frontend/app/framework/angular/forms/editors/checkbox-group.component.html

@ -1,7 +1,7 @@
<div #container (sqxResized)="updateContainerWidth($event.width)"> <div #container (sqxResized)="updateContainerWidth($event.width)">
<div class="form-check" *ngFor="let value of valuesSorted" <div class="form-check" *ngFor="let value of valuesSorted; trackBy: trackByValue"
[class.form-check-block]="!snapshot.isSingleLine" [class.form-check-block]="!snapshot.isSingleline"
[class.form-check-inline]="snapshot.isSingleLine"> [class.form-check-inline]="snapshot.isSingleline">
<input class="form-check-input" type="checkbox" id="{{controlId}}_{{value}}" <input class="form-check-input" type="checkbox" id="{{controlId}}_{{value}}"
(blur)="callTouched()" (blur)="callTouched()"
(change)="check($event!.target!.checked!, value)" (change)="check($event!.target!.checked!, value)"

20
frontend/app/framework/angular/forms/editors/checkbox-group.component.ts

@ -20,7 +20,7 @@ interface State {
checkedValues: ReadonlyArray<TagValue>; checkedValues: ReadonlyArray<TagValue>;
// True when all checkboxes can be shown as single line. // True when all checkboxes can be shown as single line.
isSingleLine?: boolean; isSingleline?: boolean;
} }
@Component({ @Component({
@ -43,6 +43,9 @@ export class CheckboxGroupComponent extends StatefulControlComponent<State, stri
@ViewChild('container', { static: false }) @ViewChild('container', { static: false })
public containerElement: ElementRef<HTMLDivElement>; public containerElement: ElementRef<HTMLDivElement>;
@Input()
public layout: 'Auto' | 'Singletine' | 'Multiline' = 'Auto';
@Input() @Input()
public set disabled(value: boolean | undefined | null) { public set disabled(value: boolean | undefined | null) {
this.setDisabledState(value === true); this.setDisabledState(value === true);
@ -87,6 +90,7 @@ export class CheckboxGroupComponent extends StatefulControlComponent<State, stri
this.calculateStyle(); this.calculateStyle();
if (this.labelsMeasured) { if (this.labelsMeasured) {
this.calculateSingleLine();
return; return;
} }
@ -123,9 +127,15 @@ export class CheckboxGroupComponent extends StatefulControlComponent<State, stri
} }
private calculateSingleLine() { private calculateSingleLine() {
const isSingleLine = this.childrenWidth < this.containerWidth; let isSingleline = false;
if (this.layout !== 'Auto') {
isSingleline = this.layout === 'Singletine';
} else {
isSingleline = this.childrenWidth < this.containerWidth;
}
this.next({ isSingleLine }); this.next({ isSingleline });
} }
private calculateStyle() { private calculateStyle() {
@ -176,6 +186,10 @@ export class CheckboxGroupComponent extends StatefulControlComponent<State, stri
public isChecked(value: TagValue) { public isChecked(value: TagValue) {
return this.snapshot.checkedValues.indexOf(value) >= 0; return this.snapshot.checkedValues.indexOf(value) >= 0;
} }
public trackByValue(_index: number, tag: TagValue) {
return tag.id;
}
} }
let canvas: HTMLCanvasElement | null = null; let canvas: HTMLCanvasElement | null = null;

5
frontend/app/framework/angular/toolbar.component.html

@ -0,0 +1,5 @@
<ng-container *ngIf="toolbar.buttonsChanges | async; let buttons">
<button type="submit" class="btn btn-{{button.color}} ms-2" *ngFor="let button of buttons" (click)="button.method()" [disabled]="button.disabled">
{{ button.name| sqxTranslate }}
</button>
</ng-container>

0
frontend/app/framework/angular/toolbar.component.scss

22
frontend/app/framework/angular/toolbar.component.ts

@ -0,0 +1,22 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ToolbarService } from '@app/framework/internal';
@Component({
selector: 'sqx-toolbar',
styleUrls: ['./toolbar.component.scss'],
templateUrl: './toolbar.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ToolbarComponent {
constructor(
public readonly toolbar: ToolbarService,
) {
}
}

1
frontend/app/framework/declarations.ts

@ -77,6 +77,7 @@ export * from './angular/sync-width.directive';
export * from './angular/tab-router-link.directive'; export * from './angular/tab-router-link.directive';
export * from './angular/template-wrapper.directive'; export * from './angular/template-wrapper.directive';
export * from './angular/title.component'; export * from './angular/title.component';
export * from './angular/toolbar.component';
export * from './angular/video-player.component'; export * from './angular/video-player.component';
export * from './internal'; export * from './internal';
export * from './state'; export * from './state';

3
frontend/app/framework/internal.ts

@ -15,6 +15,7 @@ export * from './services/clipboard.service';
export * from './services/dialog.service'; export * from './services/dialog.service';
export * from './services/loading.service'; export * from './services/loading.service';
export * from './services/local-store.service'; export * from './services/local-store.service';
export * from './services/localizer.service';
export * from './services/message-bus.service'; export * from './services/message-bus.service';
export * from './services/onboarding.service'; export * from './services/onboarding.service';
export * from './services/resize.service'; export * from './services/resize.service';
@ -22,7 +23,7 @@ export * from './services/resource-loader.service';
export * from './services/shortcut.service'; export * from './services/shortcut.service';
export * from './services/temp.service'; export * from './services/temp.service';
export * from './services/title.service'; export * from './services/title.service';
export * from './services/localizer.service'; export * from './services/toolbar.service';
export * from './utils/array-helper'; export * from './utils/array-helper';
export * from './utils/cookies'; export * from './utils/cookies';
export * from './utils/date-helper'; export * from './utils/date-helper';

4
frontend/app/framework/module.ts

@ -11,7 +11,7 @@ import { ModuleWithProviders, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { ColorPickerModule } from 'ngx-color-picker'; import { ColorPickerModule } from 'ngx-color-picker';
import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterceptor, CanDeactivateGuard, CheckboxGroupComponent, ClipboardService, CodeComponent, CodeEditorComponent, ColorPickerComponent, ConfirmClickDirective, ControlErrorsComponent, CopyDirective, DarkenPipe, DatePipe, DateTimeEditorComponent, DayOfWeekPipe, DayPipe, DialogRendererComponent, DialogService, DisplayNamePipe, DropdownComponent, DurationPipe, EditableTitleComponent, ExternalLinkDirective, FileDropDirective, FileSizePipe, FocusOnInitDirective, FormAlertComponent, FormErrorComponent, FormHintComponent, FromNowPipe, FullDateTimePipe, HighlightPipe, HoverBackgroundDirective, ImageSourceDirective, IndeterminateValueDirective, ISODatePipe, KeysPipe, KNumberPipe, LanguageSelectorComponent, LayoutComponent, LayoutContainerDirective, LightenPipe, ListViewComponent, LoadingInterceptor, LoadingService, LocalizedInputComponent, LocalStoreService, MarkdownInlinePipe, MarkdownPipe, MessageBus, ModalDialogComponent, ModalDirective, ModalPlacementDirective, MoneyPipe, MonthPipe, OnboardingService, OnboardingTooltipComponent, PagerComponent, ParentLinkDirective, PopupLinkDirective, ProgressBarComponent, ResizedDirective, ResizeService, ResourceLoaderService, RootViewComponent, SafeHtmlPipe, SafeResourceUrlPipe, SafeUrlPipe, ScrollActiveDirective, ShortcutComponent, ShortcutDirective, ShortcutService, ShortDatePipe, ShortTimePipe, StarsComponent, StatusIconComponent, StopClickDirective, SyncScollingDirective, SyncWidthDirective, TabRouterlinkDirective, TagEditorComponent, TemplateWrapperDirective, TempService, TitleComponent, TitleService, ToggleComponent, TooltipDirective, TransformInputDirective, TranslatePipe, VideoPlayerComponent } from './declarations'; import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterceptor, CanDeactivateGuard, CheckboxGroupComponent, ClipboardService, CodeComponent, CodeEditorComponent, ColorPickerComponent, ConfirmClickDirective, ControlErrorsComponent, CopyDirective, DarkenPipe, DatePipe, DateTimeEditorComponent, DayOfWeekPipe, DayPipe, DialogRendererComponent, DialogService, DisplayNamePipe, DropdownComponent, DurationPipe, EditableTitleComponent, ExternalLinkDirective, FileDropDirective, FileSizePipe, FocusOnInitDirective, FormAlertComponent, FormErrorComponent, FormHintComponent, FromNowPipe, FullDateTimePipe, HighlightPipe, HoverBackgroundDirective, ImageSourceDirective, IndeterminateValueDirective, ISODatePipe, KeysPipe, KNumberPipe, LanguageSelectorComponent, LayoutComponent, LayoutContainerDirective, LightenPipe, ListViewComponent, LoadingInterceptor, LoadingService, LocalizedInputComponent, LocalStoreService, MarkdownInlinePipe, MarkdownPipe, MessageBus, ModalDialogComponent, ModalDirective, ModalPlacementDirective, MoneyPipe, MonthPipe, OnboardingService, OnboardingTooltipComponent, PagerComponent, ParentLinkDirective, PopupLinkDirective, ProgressBarComponent, ResizedDirective, ResizeService, ResourceLoaderService, RootViewComponent, SafeHtmlPipe, SafeResourceUrlPipe, SafeUrlPipe, ScrollActiveDirective, ShortcutComponent, ShortcutDirective, ShortcutService, ShortDatePipe, ShortTimePipe, StarsComponent, StatusIconComponent, StopClickDirective, SyncScollingDirective, SyncWidthDirective, TabRouterlinkDirective, TagEditorComponent, TemplateWrapperDirective, TempService, TitleComponent, TitleService, ToggleComponent, ToolbarComponent, TooltipDirective, TransformInputDirective, TranslatePipe, VideoPlayerComponent } from './declarations';
@NgModule({ @NgModule({
imports: [ imports: [
@ -94,6 +94,7 @@ import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterc
TagEditorComponent, TagEditorComponent,
TemplateWrapperDirective, TemplateWrapperDirective,
TitleComponent, TitleComponent,
ToolbarComponent,
ToggleComponent, ToggleComponent,
TooltipDirective, TooltipDirective,
TransformInputDirective, TransformInputDirective,
@ -176,6 +177,7 @@ import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterc
TagEditorComponent, TagEditorComponent,
TemplateWrapperDirective, TemplateWrapperDirective,
TitleComponent, TitleComponent,
ToolbarComponent,
ToggleComponent, ToggleComponent,
TooltipDirective, TooltipDirective,
TransformInputDirective, TransformInputDirective,

113
frontend/app/framework/services/toolbar.service.spec.ts

@ -0,0 +1,113 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ButtonItem, ToolbarService } from './toolbar.service';
describe('ToolbarService', () => {
it('should instantiate', () => {
const toolbarService = new ToolbarService();
expect(toolbarService).toBeDefined();
});
it('should add button to toolbar', () => {
const toolbarService = new ToolbarService();
let buttons: ReadonlyArray<ButtonItem>;
let buttonsTriggered = 0;
toolbarService.buttonsChanges.subscribe(result => {
buttons = result;
buttonsTriggered++;
});
toolbarService.addButton(undefined, 'button1', () => {});
toolbarService.addButton(undefined, 'button2', () => {});
expect(buttons!.length).toBe(2);
expect(buttonsTriggered).toEqual(3);
});
it('should replace button in toolbar', () => {
const toolbarService = new ToolbarService();
let buttons: ReadonlyArray<ButtonItem>;
let buttonsTriggered = 0;
toolbarService.buttonsChanges.subscribe(result => {
buttons = result;
buttonsTriggered++;
});
toolbarService.addButton(undefined, 'button1', () => {});
toolbarService.addButton(undefined, 'button1', () => {}, { disabled: true });
expect(buttons!.length).toBe(1);
expect(buttonsTriggered).toEqual(3);
});
it('should not replace button in toolbar if nothing changed', () => {
const toolbarService = new ToolbarService();
let buttons: ReadonlyArray<ButtonItem>;
let buttonsTriggered = 0;
toolbarService.buttonsChanges.subscribe(result => {
buttons = result;
buttonsTriggered++;
});
const action = () => {};
toolbarService.addButton(undefined, 'button1', action);
toolbarService.addButton(undefined, 'button1', action);
expect(buttons!.length).toBe(1);
expect(buttonsTriggered).toEqual(2);
});
it('should remove buttons by owner', () => {
const toolbarService = new ToolbarService();
let buttons: ReadonlyArray<ButtonItem>;
let buttonsTriggered = 0;
const owner1 = {};
const owner2 = {};
toolbarService.buttonsChanges.subscribe(result => {
buttons = result;
buttonsTriggered++;
});
toolbarService.addButton(owner1, 'button1', () => {});
toolbarService.addButton(owner2, 'button2', () => {});
toolbarService.remove(owner1);
expect(buttons!.length).toBe(1);
expect(buttonsTriggered).toEqual(4);
});
it('should remove all buttons', () => {
const toolbarService = new ToolbarService();
let buttons: ReadonlyArray<ButtonItem>;
let buttonsTriggered = 0;
toolbarService.buttonsChanges.subscribe(result => {
buttons = result;
buttonsTriggered++;
});
toolbarService.addButton(undefined, 'button1', () => {});
toolbarService.addButton(undefined, 'button2', () => {});
toolbarService.removeAll();
expect(buttons!.length).toBe(0);
expect(buttonsTriggered).toEqual(4);
});
});

55
frontend/app/framework/services/toolbar.service.ts

@ -0,0 +1,55 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { Types } from './../utils/types';
export type ButtonItem = { owner: any; name: string; method: () => void } & ButtonOptions;
export type ButtonOptions = { disabled?: boolean; color?: string };
@Injectable()
export class ToolbarService {
private readonly buttons$ = new BehaviorSubject<ReadonlyArray<ButtonItem>>([]);
public get buttonsChanges(): Observable<ReadonlyArray<ButtonItem>> {
return this.buttons$;
}
public addButton(owner: any, name: string, method: () => void, options?: ButtonOptions) {
const newButton = { owner, name, method, disabled: options?.disabled, color: options?.color || 'primary' };
const buttons = this.buttons$.value;
const button = buttons.find(x => x.name === name);
if (!button || !Types.equals(newButton, button)) {
const newButtons = this.buttons$.value.filter(x => x.name !== name);
newButtons.push(newButton);
this.buttons$.next(newButtons);
}
}
public remove(owner: any) {
const buttons = this.buttons$.value;
const newButtons = buttons.filter(x => x.owner !== owner);
if (newButtons.length !== buttons.length) {
this.buttons$.next(newButtons);
}
}
public removeAll() {
const buttons = this.buttons$.value;
if (buttons.length > 0) {
this.buttons$.next([]);
}
}
}

12
frontend/app/shared/state/contents.forms-helpers.ts

@ -9,7 +9,7 @@
/* eslint-disable no-useless-return */ /* eslint-disable no-useless-return */
import { AbstractControl, ValidatorFn } from '@angular/forms'; import { AbstractControl, ValidatorFn } from '@angular/forms';
import { Types } from '@app/framework'; import { getRawValue, Types } from '@app/framework';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { AppLanguageDto } from './../services/app-languages.service'; import { AppLanguageDto } from './../services/app-languages.service';
@ -302,6 +302,16 @@ export abstract class AbstractContentForm<T extends FieldDto, TForm extends Abst
this.updateCustomState(context, fieldData, itemData, state); this.updateCustomState(context, fieldData, itemData, state);
} }
public getRawValue() {
return getRawValue(this.form);
}
public setValue(value: any) {
this.prepareLoad(value);
this.form.reset(value);
}
public unset() { public unset() {
this.form.setValue(undefined); this.form.setValue(undefined);
} }

21
frontend/app/shared/state/contents.forms.ts

@ -230,27 +230,6 @@ export class FieldForm extends AbstractContentForm<RootFieldDto, FormGroup> {
this.isRequired = field.properties.isRequired; this.isRequired = field.properties.isRequired;
} }
public copyFrom(source: FieldForm, key: string) {
const target = this.get(key);
if (!target) {
return;
}
const value = source.get(key)?.form.value;
target.prepareLoad(value);
target.form.reset(source.get(key)?.form.value);
}
public copyAllFrom(source: FieldForm) {
const value = source.form.getRawValue();
this.prepareLoad(value);
this.form.reset(value);
}
public get(language: string | LanguageDto) { public get(language: string | LanguageDto) {
if (this.field.isLocalizable) { if (this.field.isLocalizable) {
return this.partitions[language['iso2Code'] || language]; return this.partitions[language['iso2Code'] || language];

1
frontend/app/theme/_forms.scss

@ -114,7 +114,6 @@
} }
.form-alert { .form-alert {
@include absolute(1.55rem, 0, auto, auto);
font-size: .9rem; font-size: .9rem;
font-weight: normal; font-weight: normal;
padding: 1rem; padding: 1rem;

Loading…
Cancel
Save