mirror of https://github.com/Squidex/squidex.git
Browse Source
* Refactor content-page actions. * Copy drop down. * File was not saved. * Getting rid of toolbar logic from content-page. * Better reuse of components. * Fix testspull/772/head
committed by
GitHub
38 changed files with 570 additions and 137 deletions
@ -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> |
||||
@ -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> |
||||
@ -0,0 +1,3 @@ |
|||||
|
.section { |
||||
|
padding: .25rem 1rem; |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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,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, |
||||
|
) { |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
}); |
||||
|
}); |
||||
@ -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([]); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue