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