Browse Source

More UI changes

pull/271/head
Sebastian Stehle 8 years ago
parent
commit
f1cbed996a
  1. 2
      src/Squidex/app/features/administration/guards/unset-user.guard.ts
  2. 8
      src/Squidex/app/features/administration/guards/user-must-exist.guard.ts
  3. 17
      src/Squidex/app/features/administration/state/users.state.ts
  4. 2
      src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.html
  5. 2
      src/Squidex/app/features/rules/pages/rules/actions/azure-queue-action.component.html
  6. 2
      src/Squidex/app/features/rules/pages/rules/actions/elastic-search-action.component.html
  7. 2
      src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.html
  8. 2
      src/Squidex/app/features/rules/pages/rules/actions/slack-action.component.html
  9. 2
      src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.html
  10. 2
      src/Squidex/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.html
  11. 2
      src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html
  12. 11
      src/Squidex/app/features/schemas/declarations.ts
  13. 44
      src/Squidex/app/features/schemas/guards/schema-must-exist.guard.ts
  14. 19
      src/Squidex/app/features/schemas/module.ts
  15. 23
      src/Squidex/app/features/schemas/pages/messages.ts
  16. 128
      src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html
  17. 65
      src/Squidex/app/features/schemas/pages/schema/field-wizard.component.scss
  18. 149
      src/Squidex/app/features/schemas/pages/schema/field-wizard.component.ts
  19. 129
      src/Squidex/app/features/schemas/pages/schema/field.component.html
  20. 87
      src/Squidex/app/features/schemas/pages/schema/field.component.ts
  21. 56
      src/Squidex/app/features/schemas/pages/schema/forms/field-form-common.component.html
  22. 2
      src/Squidex/app/features/schemas/pages/schema/forms/field-form-common.component.scss
  23. 47
      src/Squidex/app/features/schemas/pages/schema/forms/field-form-common.component.ts
  24. 29
      src/Squidex/app/features/schemas/pages/schema/forms/field-form-ui.component.html
  25. 2
      src/Squidex/app/features/schemas/pages/schema/forms/field-form-ui.component.scss
  26. 25
      src/Squidex/app/features/schemas/pages/schema/forms/field-form-ui.component.ts
  27. 29
      src/Squidex/app/features/schemas/pages/schema/forms/field-form-validation.component.html
  28. 2
      src/Squidex/app/features/schemas/pages/schema/forms/field-form-validation.component.scss
  29. 25
      src/Squidex/app/features/schemas/pages/schema/forms/field-form-validation.component.ts
  30. 62
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.html
  31. 21
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.scss
  32. 242
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts
  33. 31
      src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts
  34. 7
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html
  35. 105
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts
  36. 201
      src/Squidex/app/features/schemas/state/schemas.state.ts
  37. 2
      src/Squidex/app/shared/declarations-base.ts
  38. 4
      src/Squidex/app/shared/services/apps-store.service.ts
  39. 39
      src/Squidex/app/shared/services/schemas.service.ts
  40. 65
      src/Squidex/app/shared/state/apps.state.ts
  41. 1
      src/Squidex/app/shared/utils/messages.ts
  42. 1
      src/Squidex/app/theme/_bootstrap.scss
  43. 2
      src/Squidex/app/theme/_rules.scss
  44. 17
      src/Squidex/app/theme/icomoon/icons/type-Tags.svg

2
src/Squidex/app/features/administration/guards/unset-user.guard.ts

@ -19,6 +19,6 @@ export class UnsetUserGuard implements CanActivate {
}
public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
return this.usersState.selectUser(null).map(r => !r);
return this.usersState.selectUser(null).map(u => u === null);
}
}

8
src/Squidex/app/features/administration/guards/user-must-exist.guard.ts

@ -31,17 +31,13 @@ export class UserMustExistGuard implements CanActivate {
}
const result =
this.usersState.selectUser(userId).map(u => !!u)
this.usersState.selectUser(userId)
.do(dto => {
if (!dto) {
this.router.navigate(['/404']);
}
})
.catch(error => {
this.router.navigate(['/404']);
return Observable.of(false);
});
.map(u => u !== null);
return result;
}

17
src/Squidex/app/features/administration/state/users.state.ts

@ -26,7 +26,7 @@ import {
@Injectable()
export class UsersState {
public usersItems = new BehaviorSubject<ImmutableArray<UserDto>>(ImmutableArray.empty());
public users = new BehaviorSubject<ImmutableArray<UserDto>>(ImmutableArray.empty());
public usersPager = new BehaviorSubject(new Pager(0));
public usersQuery = new BehaviorSubject('');
@ -38,11 +38,11 @@ export class UsersState {
) {
}
public selectUser(id: string | null): Observable<boolean> {
public selectUser(id: string | null): Observable<UserDto | null> {
const observable =
!id ?
Observable.of(null) :
Observable.of(this.usersItems.value.find(x => x.id === id))
Observable.of(this.users.value.find(x => x.id === id))
.switchMap(user => {
if (!user) {
return this.usersService.getUser(id).catch(() => Observable.of(null));
@ -54,23 +54,22 @@ export class UsersState {
return observable
.do(user => {
this.selectedUser.next(user);
})
.map(u => u !== null);
});
}
public loadUsers(): Observable<any> {
return this.usersService.getUsers(this.usersPager.value.pageSize, this.usersPager.value.skip, this.usersQuery.value)
.catch(error => this.notifyError(error))
.do(dtos => {
this.usersItems.nextBy(v => ImmutableArray.of(dtos.items));
this.users.nextBy(v => ImmutableArray.of(dtos.items));
this.usersPager.nextBy(v => v.setCount(dtos.total));
});
}
public createUser(request: CreateUserDto): Observable<UserDto> {
return this.usersService.postUser(request)
.do(user => {
this.usersItems.nextBy(v => v.pushFront(user));
.do(dto => {
this.users.nextBy(v => v.pushFront(dto));
this.usersPager.nextBy(v => v.incrementCount());
});
}
@ -124,7 +123,7 @@ export class UsersState {
}
private replaceUser(user: UserDto) {
this.usersItems.nextBy(v => v.replaceBy('id', user));
this.users.nextBy(v => v.replaceBy('id', user));
this.selectedUser.nextBy(v => v !== null && v.id === user.id ? user : v);
}

2
src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.html

@ -1,4 +1,4 @@
<h3 class="rule-title">Populate index in algolia with content</h3>
<h3 class="wizard-title">Populate index in algolia with content</h3>
<form [formGroup]="actionForm" class="form-horizontal" (ngSubmit)="save()">
<div class="form-group row">

2
src/Squidex/app/features/rules/pages/rules/actions/azure-queue-action.component.html

@ -1,4 +1,4 @@
<h3 class="rule-title">Send event payload to Azure Storage Queue</h3>
<h3 class="wizard-title">Send event payload to Azure Storage Queue</h3>
<form [formGroup]="actionForm" class="form-horizontal" (ngSubmit)="save()">
<div class="form-group row">

2
src/Squidex/app/features/rules/pages/rules/actions/elastic-search-action.component.html

@ -1,4 +1,4 @@
<h3 class="rule-title">Populate index in ElasticSearch with content</h3>
<h3 class="wizard-title">Populate index in ElasticSearch with content</h3>
<form [formGroup]="actionForm" class="form-horizontal" (ngSubmit)="save()">
<div class="form-group row">

2
src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.html

@ -1,4 +1,4 @@
<h3 class="rule-title">Purge cache entries in Fastly</h3>
<h3 class="wizard-title">Purge cache entries in Fastly</h3>
<form [formGroup]="actionForm" class="form-horizontal" (ngSubmit)="save()">
<div class="form-group row">

2
src/Squidex/app/features/rules/pages/rules/actions/slack-action.component.html

@ -1,4 +1,4 @@
<h3 class="rule-title">Send custom text to an incoming webhook in Slack</h3>
<h3 class="wizard-title">Send custom text to an incoming webhook in Slack</h3>
<form [formGroup]="actionForm" class="form-horizontal" (ngSubmit)="save()">
<div class="form-group row">

2
src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.html

@ -1,4 +1,4 @@
<h3 class="rule-title">Send event payload to webhook</h3>
<h3 class="wizard-title">Send event payload to webhook</h3>
<form [formGroup]="actionForm" class="form-horizontal" (ngSubmit)="save()">
<div class="form-group row">

2
src/Squidex/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.html

@ -1,4 +1,4 @@
<h3 class="rule-title">Trigger rule when asset has been...</h3>
<h3 class="wizard-title">Trigger rule when asset has been...</h3>
<form [formGroup]="triggerForm" class="form-horizontal" (ngSubmit)="save()">

2
src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html

@ -1,4 +1,4 @@
<h3 class="rule-title">Trigger rule when an events for a schemas happens</h3>
<h3 class="wizard-title">Trigger rule when an events for a schemas happens</h3>
<table class="table table-middle table-sm table-fixed table-borderless" *ngIf="!handleAll">
<colgroup>

11
src/Squidex/app/features/schemas/declarations.ts

@ -5,6 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
export * from './guards/schema-must-exist.guard';
export * from './pages/schema/types/assets-ui.component';
export * from './pages/schema/types/assets-validation.component';
export * from './pages/schema/types/boolean-ui.component';
@ -24,10 +26,17 @@ export * from './pages/schema/types/string-validation.component';
export * from './pages/schema/types/tags-ui.component';
export * from './pages/schema/types/tags-validation.component';
export * from './pages/schema/forms/field-form-common.component';
export * from './pages/schema/forms/field-form-ui.component';
export * from './pages/schema/forms/field-form-validation.component';
export * from './pages/schema/field.component';
export * from './pages/schema/field-wizard.component';
export * from './pages/schema/schema-edit-form.component';
export * from './pages/schema/schema-page.component';
export * from './pages/schema/schema-scripts-form.component';
export * from './pages/schemas/schema-form.component';
export * from './pages/schemas/schemas-page.component';
export * from './pages/schemas/schemas-page.component';
export * from './state/schemas.state';

44
src/Squidex/app/features/schemas/guards/schema-must-exist.guard.ts

@ -0,0 +1,44 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { allParams } from 'framework';
import { SchemasState } from './../state/schemas.state';
@Injectable()
export class SchemaMustExistGuard implements CanActivate {
constructor(
private readonly schemasState: SchemasState,
private readonly router: Router
) {
}
public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
const params = allParams(route);
const schemaName = params['schemaName'];
if (!schemaName) {
throw 'Route must contain schema name.';
}
const result =
this.schemasState.selectSchema(schemaName)
.do(dto => {
if (!dto) {
this.router.navigate(['/404']);
}
})
.map(u => u !== null);
return result;
}
}

19
src/Squidex/app/features/schemas/module.ts

@ -12,7 +12,6 @@ import { DndModule } from 'ng2-dnd';
import {
HelpComponent,
HistoryComponent,
ResolveSchemaGuard,
SqxFrameworkModule,
SqxSharedModule
} from 'shared';
@ -25,6 +24,10 @@ import {
BooleanValidationComponent,
DateTimeUIComponent,
DateTimeValidationComponent,
FieldFormCommonComponent,
FieldFormUIComponent,
FieldFormValidationComponent,
FieldWizardComponent,
GeolocationUIComponent,
GeolocationValidationComponent,
JsonUIComponent,
@ -35,9 +38,11 @@ import {
ReferencesValidationComponent,
SchemaEditFormComponent,
SchemaFormComponent,
SchemaMustExistGuard,
SchemaPageComponent,
SchemasPageComponent,
SchemaScriptsFormComponent,
SchemasState,
StringUIComponent,
StringValidationComponent,
TagsUIComponent,
@ -55,9 +60,7 @@ const routes: Routes = [
{
path: ':schemaName',
component: SchemaPageComponent,
resolve: {
schema: ResolveSchemaGuard
},
canActivate: [SchemaMustExistGuard],
children: [
{
path: 'history',
@ -85,6 +88,10 @@ const routes: Routes = [
DndModule,
RouterModule.forChild(routes)
],
providers: [
SchemaMustExistGuard,
SchemasState
],
declarations: [
FieldComponent,
AssetsUIComponent,
@ -93,6 +100,10 @@ const routes: Routes = [
BooleanValidationComponent,
DateTimeUIComponent,
DateTimeValidationComponent,
FieldFormCommonComponent,
FieldFormUIComponent,
FieldFormValidationComponent,
FieldWizardComponent,
GeolocationUIComponent,
GeolocationValidationComponent,
JsonUIComponent,

23
src/Squidex/app/features/schemas/pages/messages.ts

@ -5,29 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { SchemaDto } from 'shared';
export class SchemaUpdated {
constructor(
public readonly schema: SchemaDto
) {
}
}
export class SchemaCreated {
constructor(
public readonly schema: SchemaDto
) {
}
}
export class SchemaDeleted {
constructor(
public readonly schema: SchemaDto
) {
}
}
export class SchemaCloning {
constructor(
public readonly importing: any

128
src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html

@ -0,0 +1,128 @@
<div class="modal-dialog modal-lg">
<div class="modal-content" *ngIf="!editForm">
<form [formGroup]="addFieldForm" (ngSubmit)="addField(false, false)">
<div class="modal-header">
<h4 class="modal-title">
Create Field
</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="complete()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<h3 class="wizard-title">Create Field</h3>
<div *ngIf="addFieldError">
<div class="form-alert form-alert-error" [innerHTML]="addFieldError"></div>
</div>
<div class="form-group">
<div class="row">
<div class="col-4 type" *ngFor="let fieldType of fieldTypes">
<label>
<input type="radio" class="radio-input" formControlName="type" value="{{fieldType.type}}" />
<div class="row no-gutters">
<div class="col col-auto">
<div class="type-icon" [class.active]="addFieldForm.controls.type.value === fieldType.type">
<i class="icon-type-{{fieldType.type}}"></i>
</div>
</div>
<div class="col">
<div class="type-title">{{fieldType.type}}</div>
<div class="type-text text-muted">{{fieldType.description}}</div>
</div>
</div>
</label>
</div>
</div>
</div>
<div class="form-group">
<sqx-control-errors for="name" [submitted]="addFieldFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" formControlName="name" maxlength="40" placeholder="Enter field name" sqxFocusOnInit />
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="isLocalizable" formControlName="isLocalizable" />
<label class="form-check-label" for="isLocalizable">
Localizable
</label>
</div>
<small class="form-text text-muted">
You can the field as localizable. It means that is dependent on the language, for example a city name.
</small>
</div>
</div>
<div class="modal-footer">
<div class="clearfix">
<button type="reset" class="float-left btn btn-secondary" (click)="complete()">Cancel</button>
<div class="float-right">
<button class="btn btn-success" (click)="addField(false, false)">Create and close</button>
<button class="btn btn-success" (click)="addField(true, false)">Create and new field</button>
<button class="btn btn-success" (click)="addField(false, true)">Create and configure</button>
</div>
</div>
</div>
</form>
</div>
<div class="modal-content" *ngIf="editForm">
<form [formGroup]="editForm">
<div class="modal-header">
<h4 class="modal-title" *ngIf="editStep === 1">
Step 1 of 3: Common Properties
</h4>
<h4 class="modal-title" *ngIf="editStep === 2">
Step 2 of 3: Validation
</h4>
<h4 class="modal-title" *ngIf="editStep === 3">
Step 3 of 3: Editing Settings
</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="complete()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<h3 class="wizard-title" *ngIf="editStep === 1">Common</h3>
<h3 class="wizard-title" *ngIf="editStep === 2">Validation</h3>
<h3 class="wizard-title" *ngIf="editStep === 3">Editing</h3>
<div [class.hidden]="editStep !== 1">
<sqx-field-form-common [editForm]="editForm" [editFormSubmitted]="editFormSubmitted" [showName]="false" [field]="editField"></sqx-field-form-common>
</div>
<div [class.hidden]="editStep !== 2">
<sqx-field-form-validation [editForm]="editForm" [field]="editField"></sqx-field-form-validation>
</div>
<div [class.hidden]="editStep !== 3">
<sqx-field-form-ui [editForm]="editForm" [field]="editField"></sqx-field-form-ui>
</div>
</div>
<div class="modal-footer">
<div class="clearfix">
<button type="reset" class="float-left btn btn-secondary" (click)="complete()">Cancel</button>
<div class="float-right" *ngIf="editStep !== 2">
<button class="btn btn-primary" (click)="next()">Next</button>
</div>
<div class="float-right" *ngIf="editStep === 2">
<button class="btn btn-success" (click)="configureField(false)">Configure and close</button>
<button class="btn btn-success" (click)="configureField(true)">Configure and new field</button>
</div>
</div>
</div>
</form>
</div>
</div>

65
src/Squidex/app/features/schemas/pages/schema/field-wizard.component.scss

@ -0,0 +1,65 @@
@import '_vars';
@import '_mixins';
$icon-size: 4.5rem;
.form-check {
margin-top: .4rem;
margin-bottom: -.2rem;
}
.type {
& {
margin-bottom: .5rem;
}
&-title {
font-weight: bold;
}
&-text {
font-size: .9rem;
}
&-icon {
& {
@include border-radius;
height: $icon-size;
color: $color-theme-blue;
cursor: pointer;
border: 1px solid $color-border;
background: transparent;
margin-right: .5rem;
line-height: $icon-size;
font-size: 1.75rem;
font-weight: normal;
text-align: center;
width: $icon-size;
}
.radio-input {
display: none;
}
&.active {
& {
@include box-shadow(0, 0, 10px, .5);
background: $color-theme-blue;
border-color: $color-theme-blue;
color: $color-dark-foreground;
}
&:hover {
color: $color-dark-foreground;
}
}
&:hover {
border-color: $color-border-dark;
}
}
.radio-input {
display: none;
}
}

149
src/Squidex/app/features/schemas/pages/schema/field-wizard.component.ts

@ -0,0 +1,149 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormBuilder, Validators, FormGroup } from '@angular/forms';
import {
AddFieldDto,
AppContext,
createProperties,
fadeAnimation,
FieldDto,
fieldTypes,
SchemaDetailsDto,
UpdateFieldDto,
ValidatorsEx
} from 'shared';
import { SchemasState } from './../../state/schemas.state';
@Component({
selector: 'sqx-field-wizard',
styleUrls: ['./field-wizard.component.scss'],
templateUrl: './field-wizard.component.html',
providers: [
AppContext
],
animations: [
fadeAnimation
]
})
export class FieldWizardComponent {
public fieldTypes = fieldTypes;
public editFormSubmitted = false;
public editStep = 0;
public editForm: FormGroup | null;
public editField: FieldDto | null;
public addFieldError = '';
public addFieldFormSubmitted = false;
public addFieldForm =
this.formBuilder.group({
type: ['String',
[
Validators.required
]],
name: ['',
[
Validators.required,
Validators.maxLength(40),
ValidatorsEx.pattern('[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*', 'Name must be a valid javascript name in camel case.')
]],
isLocalizable: [false]
});
@Input()
public schema: SchemaDetailsDto;
@Output()
public completed = new EventEmitter();
constructor(
private readonly formBuilder: FormBuilder,
private readonly schemasState: SchemasState
) {
}
public complete() {
this.completed.emit();
}
public next() {
this.editStep++;
}
public addField(next: boolean, configure: boolean) {
this.addFieldFormSubmitted = true;
if (this.addFieldForm.valid) {
this.addFieldForm.disable();
const properties = createProperties(this.addFieldForm.controls['type'].value);
const partitioning =
this.addFieldForm.controls['isLocalizable'].value ?
'language' :
'invariant';
const requestDto = new AddFieldDto(this.addFieldForm.controls['name'].value, partitioning, properties);
this.schemasState.addField(this.schema, requestDto)
.subscribe(dto => {
this.resetFieldForm();
if (configure) {
this.editField = dto;
this.editStep = 1;
this.editForm = new FormGroup({});
} else if (!next) {
this.complete();
}
}, error => {
this.resetFieldForm(error.displayMessage);
});
}
}
public configureField(next: boolean) {
this.editFormSubmitted = true;
if (this.editForm!.valid) {
const properties = createProperties(this.editField!.properties['fieldType'], this.editForm!.value);
this.schemasState.updateField(this.schema, this.editField!, new UpdateFieldDto(properties))
.subscribe(() => {
this.resetEditForm();
if (this.next) {
this.editField = null;
this.editForm = null;
this.editStep = 1;
} else {
this.complete();
}
}, error => {
this.resetEditForm();
});
}
}
private resetEditForm() {
this.editForm!.enable();
this.editForm!.reset(this.editField!.properties);
this.editFormSubmitted = false;
}
private resetFieldForm(error = '') {
this.addFieldError = error;
this.addFieldForm.enable();
this.addFieldForm.reset({ type: 'String' });
this.addFieldFormSubmitted = false;
}
}

129
src/Squidex/app/features/schemas/pages/schema/field.component.html

@ -5,7 +5,7 @@
<span class="field-name">
<i class="field-icon icon-type-{{field.properties.fieldType}}"></i>
<span [class.field-hidden]="field.isHidden" [attr.title]="field.isHidden ? 'Hidden Field' : 'Visible Field'">{{displayName}}</span>
<span [class.field-hidden]="field.isHidden" [attr.title]="field.isHidden ? 'Hidden Field' : 'Visible Field'">{{field.displayName}}</span>
<span class="field-partitioning" *ngIf="field.isLocalizable">localizable</span>
</span>
</div>
@ -27,26 +27,26 @@
<i class="icon-dots"></i>
</button>
<div class="dropdown-menu" *sqxModalView="dropdown" [sqxModalTarget]="optionsButton" @fade>
<a class="dropdown-item" (click)="enabling.emit()" *ngIf="field.isDisabled" [class.disabled]="field.isLocked">
<a class="dropdown-item" (click)="enableField()" *ngIf="field.isDisabled" [class.disabled]="field.isLocked">
Enable
</a>
<a class="dropdown-item" (click)="disabling.emit()" *ngIf="!field.isDisabled" [class.disabled]="field.isLocked">
<a class="dropdown-item" (click)="disableField()" *ngIf="!field.isDisabled" [class.disabled]="field.isLocked">
Disable
</a>
<a class="dropdown-item" (click)="hiding.emit()" *ngIf="!field.isHidden" [class.disabled]="field.isLocked">
<a class="dropdown-item" (click)="hideField()" *ngIf="!field.isHidden" [class.disabled]="field.isLocked">
Hide
</a>
<a class="dropdown-item" (click)="showing.emit()" *ngIf="field.isHidden" [class.disabled]="field.isLocked">
<a class="dropdown-item" (click)="showField()" *ngIf="field.isHidden" [class.disabled]="field.isLocked">
Show
</a>
<a class="dropdown-item" *ngIf="!field.isLocked"
(sqxConfirmClick)="locking.emit()"
(sqxConfirmClick)="lockField()"
confirmTitle="Lock field"
confirmText="Do you really want to lock the field? Lock fields cannot be deleted or changed.">
Lock
</a>
<a class="dropdown-item dropdown-item-delete" [class.disabled]="field.isLocked"
(sqxConfirmClick)="deleting.emit()"
(sqxConfirmClick)="deleteField()"
confirmTitle="Delete field"
confirmText="Do you really want to delete the field?">
Delete
@ -80,124 +80,15 @@
</div>
<div class="table-items-row-details-tab" [class.hidden]="selectedTab !== 0">
<div class="form-group row">
<label class="col col-3 col-form-label" for="fieldName">Name</label>
<div class="col col-6">
<input type="text" class="form-control" id="fieldName" readonly [ngModel]="field.name" [ngModelOptions]="{standalone: true}" />
<small class="form-text text-muted">
The name of the field in the API response.
</small>
</div>
</div>
<div class="form-group row">
<label class="col col-3 col-form-label" for="fieldLabel">Label</label>
<div class="col col-6">
<sqx-control-errors for="label" [submitted]="editFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="fieldLabel" maxlength="100" formControlName="label" />
<small class="form-text text-muted">
Define the display name for the field for documentation and user interfaces.
</small>
</div>
</div>
<div class="form-group row">
<label class="col col-3 col-form-label" for="fieldHints">Hints</label>
<div class="col col-6">
<sqx-control-errors for="hints" [submitted]="editFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="fieldHints" maxlength="100" formControlName="hints" />
<small class="form-text text-muted">
Define some hints for the user and editor for the field for documentation and user interfaces.
</small>
</div>
</div>
<div class="form-group row">
<div class="col col-9 offset-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="fieldListfield" formControlName="isListField" />
<label class="form-check-label" for="fieldListfield">
List Field
</label>
</div>
<small class="form-text text-muted">
List fields are shown as a column in the content list. If no list field is defined, the first field is shown by default.
</small>
</div>
</div>
<sqx-field-form-common [editForm]="editForm" [editFormSubmitted]="editFormSubmitted" [field]="field"></sqx-field-form-common>
</div>
<div class="table-items-row-details-tab" [class.hidden]="selectedTab !== 1">
<div [ngSwitch]="field.properties.fieldType">
<div *ngSwitchCase="'Number'">
<sqx-number-validation [editForm]="editForm" [properties]="field.properties"></sqx-number-validation>
</div>
<div *ngSwitchCase="'String'">
<sqx-string-validation [editForm]="editForm" [properties]="field.properties" [regexSuggestions]="regexSuggestions"></sqx-string-validation>
</div>
<div *ngSwitchCase="'Boolean'">
<sqx-boolean-validation [editForm]="editForm" [properties]="field.properties"></sqx-boolean-validation>
</div>
<div *ngSwitchCase="'DateTime'">
<sqx-date-time-validation [editForm]="editForm" [properties]="field.properties"></sqx-date-time-validation>
</div>
<div *ngSwitchCase="'Geolocation'">
<sqx-geolocation-validation [editForm]="editForm" [properties]="field.properties"></sqx-geolocation-validation>
</div>
<div *ngSwitchCase="'Json'">
<sqx-json-validation [editForm]="editForm" [properties]="field.properties"></sqx-json-validation>
</div>
<div *ngSwitchCase="'Assets'">
<sqx-assets-validation [editForm]="editForm" [properties]="field.properties"></sqx-assets-validation>
</div>
<div *ngSwitchCase="'References'">
<sqx-references-validation [editForm]="editForm" [properties]="field.properties" [schemas]="schemas"></sqx-references-validation>
</div>
<div *ngSwitchCase="'Tags'">
<sqx-tags-validation [editForm]="editForm" [properties]="field.properties"></sqx-tags-validation>
</div>
</div>
<sqx-field-form-validation [editForm]="editForm" [field]="field"></sqx-field-form-validation>
</div>
<div class="table-items-row-details-tab" [class.hidden]="selectedTab !== 2">
<div [ngSwitch]="field.properties.fieldType">
<div *ngSwitchCase="'Number'">
<sqx-number-ui [editForm]="editForm" [properties]="field.properties"></sqx-number-ui>
</div>
<div *ngSwitchCase="'String'">
<sqx-string-ui [editForm]="editForm" [properties]="field.properties"></sqx-string-ui>
</div>
<div *ngSwitchCase="'Boolean'">
<sqx-boolean-ui [editForm]="editForm" [properties]="field.properties"></sqx-boolean-ui>
</div>
<div *ngSwitchCase="'DateTime'">
<sqx-date-time-ui [editForm]="editForm" [properties]="field.properties"></sqx-date-time-ui>
</div>
<div *ngSwitchCase="'Geolocation'">
<sqx-geolocation-ui [editForm]="editForm" [properties]="field.properties"></sqx-geolocation-ui>
</div>
<div *ngSwitchCase="'Json'">
<sqx-json-ui [editForm]="editForm" [properties]="field.properties"></sqx-json-ui>
</div>
<div *ngSwitchCase="'Assets'">
<sqx-assets-ui [editForm]="editForm" [properties]="field.properties"></sqx-assets-ui>
</div>
<div *ngSwitchCase="'References'">
<sqx-references-ui [editForm]="editForm" [properties]="field.properties"></sqx-references-ui>
</div>
<div *ngSwitchCase="'Tags'">
<sqx-tags-ui [editForm]="editForm" [properties]="field.properties"></sqx-tags-ui>
</div>
</div>
<sqx-field-form-ui [editForm]="editForm" [field]="field"></sqx-field-form-ui>
</div>
</form>
</div>

87
src/Squidex/app/features/schemas/pages/schema/field.component.ts

@ -5,19 +5,20 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { Component, Input, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
import {
AppPatternDto,
createProperties,
fadeAnimation,
FieldDto,
FieldPropertiesDto,
ModalView,
SchemaDto
SchemaDetailsDto,
UpdateFieldDto
} from 'shared';
import { SchemasState } from './../../state/schemas.state';
@Component({
selector: 'sqx-field',
styleUrls: ['./field.component.scss'],
@ -31,58 +32,18 @@ export class FieldComponent implements OnInit {
public field: FieldDto;
@Input()
public schemas: SchemaDto[];
@Input()
public regexSuggestions: AppPatternDto[] = [];
@Output()
public locking = new EventEmitter();
@Output()
public hiding = new EventEmitter();
@Output()
public showing = new EventEmitter();
@Output()
public saving = new EventEmitter<FieldPropertiesDto>();
@Output()
public enabling = new EventEmitter();
@Output()
public disabling = new EventEmitter();
@Output()
public deleting = new EventEmitter();
public schema: SchemaDetailsDto;
public dropdown = new ModalView(false, true);
public isEditing = false;
public selectedTab = 0;
public get displayName() {
return this.field.properties.label && this.field.properties.label.length > 0 ? this.field.properties.label : this.field.name;
}
public editFormSubmitted = false;
public editForm =
this.formBuilder.group({
label: ['',
[
Validators.maxLength(100)
]],
hints: ['',
[
Validators.maxLength(100)
]],
isRequired: [false],
isListField: [false]
});
public editForm = new FormGroup({});
constructor(
private readonly formBuilder: FormBuilder
private readonly schemasState: SchemasState
) {
}
@ -102,20 +63,40 @@ export class FieldComponent implements OnInit {
this.resetEditForm();
}
public deleteField() {
this.schemasState.deleteField(this.schema, this.field).subscribe();
}
public enableField() {
this.schemasState.enableField(this.schema, this.field).subscribe();
}
public disableField() {
this.schemasState.disableField(this.schema, this.field).subscribe();
}
public showField() {
this.schemasState.showField(this.schema, this.field).subscribe();
}
public hideField() {
this.schemasState.hideField(this.schema, this.field).subscribe();
}
public lockField() {
this.schemasState.lockField(this.schema, this.field).subscribe();
}
public save() {
this.editFormSubmitted = true;
if (this.editForm.valid) {
const properties = createProperties(this.field.properties['fieldType'], this.editForm.value);
this.emitSaving(properties);
this.schemasState.updateField(this.schema, this.field, new UpdateFieldDto(properties)).subscribe();
}
}
private emitSaving(properties: FieldPropertiesDto) {
this.saving.emit(properties);
}
private resetEditForm() {
this.editFormSubmitted = false;
this.editForm.reset(this.field.properties);

56
src/Squidex/app/features/schemas/pages/schema/forms/field-form-common.component.html

@ -0,0 +1,56 @@
<div [formGroup]="editForm">
<div class="form-group row" *ngIf="showName">
<label class="col col-3 col-form-label" for="fieldName">Name</label>
<div class="col col-6">
<input type="text" class="form-control" id="fieldName" readonly [ngModel]="field.name" [ngModelOptions]="{standalone: true}" />
<small class="form-text text-muted">
The name of the field in the API response.
</small>
</div>
</div>
<div class="form-group row">
<label class="col col-3 col-form-label" for="fieldLabel">Label</label>
<div class="col col-6">
<sqx-control-errors for="label" [submitted]="editFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="fieldLabel" maxlength="100" formControlName="label" />
<small class="form-text text-muted">
Define the display name for the field for documentation and user interfaces.
</small>
</div>
</div>
<div class="form-group row">
<label class="col col-3 col-form-label" for="fieldHints">Hints</label>
<div class="col col-6">
<sqx-control-errors for="hints" [submitted]="editFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="fieldHints" maxlength="100" formControlName="hints" />
<small class="form-text text-muted">
Define some hints for the user and editor for the field for documentation and user interfaces.
</small>
</div>
</div>
<div class="form-group row">
<div class="col col-9 offset-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="fieldListfield" formControlName="isListField" />
<label class="form-check-label" for="fieldListfield">
List Field
</label>
</div>
<small class="form-text text-muted">
List fields are shown as a column in the content list. If no list field is defined, the first field is shown by default.
</small>
</div>
</div>
</div>

2
src/Squidex/app/features/schemas/pages/schema/forms/field-form-common.component.scss

@ -0,0 +1,2 @@
@import '_vars';
@import '_mixins';

47
src/Squidex/app/features/schemas/pages/schema/forms/field-form-common.component.ts

@ -0,0 +1,47 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { FieldDto } from 'shared';
@Component({
selector: 'sqx-field-form-common',
styleUrls: ['field-form-common.component.scss'],
templateUrl: 'field-form-common.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FieldFormCommonComponent implements OnInit {
@Input()
public editForm: FormGroup;
@Input()
public editFormSubmitted = false;
@Input()
public showName = true;
@Input()
public field: FieldDto;
public ngOnInit() {
this.editForm.setControl('label',
new FormControl(this.field.properties.label,
Validators.maxLength(100)));
this.editForm.setControl('hints',
new FormControl(this.field.properties.label,
Validators.maxLength(100)));
this.editForm.setControl('isRequired',
new FormControl(this.field.properties.isRequired));
this.editForm.setControl('isListField',
new FormControl(this.field.properties.isListField));
}
}

29
src/Squidex/app/features/schemas/pages/schema/forms/field-form-ui.component.html

@ -0,0 +1,29 @@
<div [ngSwitch]="field.properties.fieldType">
<div *ngSwitchCase="'Number'">
<sqx-number-ui [editForm]="editForm" [properties]="field.properties"></sqx-number-ui>
</div>
<div *ngSwitchCase="'String'">
<sqx-string-ui [editForm]="editForm" [properties]="field.properties"></sqx-string-ui>
</div>
<div *ngSwitchCase="'Boolean'">
<sqx-boolean-ui [editForm]="editForm" [properties]="field.properties"></sqx-boolean-ui>
</div>
<div *ngSwitchCase="'DateTime'">
<sqx-date-time-ui [editForm]="editForm" [properties]="field.properties"></sqx-date-time-ui>
</div>
<div *ngSwitchCase="'Geolocation'">
<sqx-geolocation-ui [editForm]="editForm" [properties]="field.properties"></sqx-geolocation-ui>
</div>
<div *ngSwitchCase="'Json'">
<sqx-json-ui [editForm]="editForm" [properties]="field.properties"></sqx-json-ui>
</div>
<div *ngSwitchCase="'Assets'">
<sqx-assets-ui [editForm]="editForm" [properties]="field.properties"></sqx-assets-ui>
</div>
<div *ngSwitchCase="'References'">
<sqx-references-ui [editForm]="editForm" [properties]="field.properties"></sqx-references-ui>
</div>
<div *ngSwitchCase="'Tags'">
<sqx-tags-ui [editForm]="editForm" [properties]="field.properties"></sqx-tags-ui>
</div>
</div>

2
src/Squidex/app/features/schemas/pages/schema/forms/field-form-ui.component.scss

@ -0,0 +1,2 @@
@import '_vars';
@import '_mixins';

25
src/Squidex/app/features/schemas/pages/schema/forms/field-form-ui.component.ts

@ -0,0 +1,25 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { FieldDto } from 'shared';
@Component({
selector: 'sqx-field-form-ui',
styleUrls: ['field-form-ui.component.scss'],
templateUrl: 'field-form-ui.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FieldFormUIComponent {
@Input()
public editForm: FormGroup;
@Input()
public field: FieldDto;
}

29
src/Squidex/app/features/schemas/pages/schema/forms/field-form-validation.component.html

@ -0,0 +1,29 @@
<div [ngSwitch]="field.properties.fieldType">
<div *ngSwitchCase="'Number'">
<sqx-number-validation [editForm]="editForm" [properties]="field.properties"></sqx-number-validation>
</div>
<div *ngSwitchCase="'String'">
<sqx-string-validation [editForm]="editForm" [properties]="field.properties"></sqx-string-validation>
</div>
<div *ngSwitchCase="'Boolean'">
<sqx-boolean-validation [editForm]="editForm" [properties]="field.properties"></sqx-boolean-validation>
</div>
<div *ngSwitchCase="'DateTime'">
<sqx-date-time-validation [editForm]="editForm" [properties]="field.properties"></sqx-date-time-validation>
</div>
<div *ngSwitchCase="'Geolocation'">
<sqx-geolocation-validation [editForm]="editForm" [properties]="field.properties"></sqx-geolocation-validation>
</div>
<div *ngSwitchCase="'Json'">
<sqx-json-validation [editForm]="editForm" [properties]="field.properties"></sqx-json-validation>
</div>
<div *ngSwitchCase="'Assets'">
<sqx-assets-validation [editForm]="editForm" [properties]="field.properties"></sqx-assets-validation>
</div>
<div *ngSwitchCase="'References'">
<sqx-references-validation [editForm]="editForm" [properties]="field.properties"></sqx-references-validation>
</div>
<div *ngSwitchCase="'Tags'">
<sqx-tags-validation [editForm]="editForm" [properties]="field.properties"></sqx-tags-validation>
</div>
</div>

2
src/Squidex/app/features/schemas/pages/schema/forms/field-form-validation.component.scss

@ -0,0 +1,2 @@
@import '_vars';
@import '_mixins';

25
src/Squidex/app/features/schemas/pages/schema/forms/field-form-validation.component.ts

@ -0,0 +1,25 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { FieldDto } from 'shared';
@Component({
selector: 'sqx-field-form-validation',
styleUrls: ['field-form-validation.component.scss'],
templateUrl: 'field-form-validation.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FieldFormValidationComponent {
@Input()
public editForm: FormGroup;
@Input()
public field: FieldDto;
}

62
src/Squidex/app/features/schemas/pages/schema/schema-page.component.html

@ -61,57 +61,12 @@
<div class="panel-main">
<div class="panel-content panel-content-scroll" dnd-sortable-container [sortableData]="schema.fields">
<div *ngFor="let field of schema.fields; let i = index" dnd-sortable [sortableIndex]="i" (sqxSorted)="sortFields($event)">
<sqx-field [field]="field" [schemas]="schemas?.values"
[regexSuggestions]="regexSuggestions"
(disabling)="disableField(field)"
(deleting)="deleteField(field)"
(enabling)="enableField(field)"
(locking)="lockField(field)"
(showing)="showField(field)"
(hiding)="hideField(field)"
(saving)="saveField(field, $event)"></sqx-field>
<sqx-field [field]="field" [schema]="schema"></sqx-field>
</div>
<div class="table-items-footer">
<form [formGroup]="addFieldForm" (ngSubmit)="addField()">
<div class="row no-gutters">
<div class="col-4">
<sqx-dropdown formControlName="type" [items]="fieldTypes">
<ng-template let-type="$implicit">
<i class="field-icon icon-type-{{type}}"></i> <span>{{type}}</span>
</ng-template>
</sqx-dropdown>
</div>
<div class="col-8 pl-1">
<sqx-control-errors for="name" [submitted]="addFieldFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" formControlName="name" maxlength="40" placeholder="Enter field name" />
</div>
</div>
<div class="row no-gutters mt-3">
<div class="col pr-2">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="isLocalizable" formControlName="isLocalizable" />
<label class="form-check-label" for="isLocalizable">
Localizable
</label>
</div>
<small class="form-text text-muted">
You can the field as localizable. It means that is dependent on the language, for example a city name.
</small>
</div>
<div class="col col-auto pl-1">
<button type="button" class="btn btn-secondary" (click)="cancelAddField()">Cancel</button>
</div>
<div class="col col-auto pl-1">
<button type="submit" class="btn btn-success" [disabled]="!hasName">Add Field</button>
</div>
</div>
</form>
</div>
<button class="btn btn-success field-button" (click)="addFieldDialog.show()">
<i class="icon icon-plus field-button-icon"></i> <div class="field-button-text">Add Field</div>
</button>
</div>
<div class="panel-sidebar">
<a class="panel-link" routerLink="history" routerLinkActive="active" #linkHistory>
@ -170,6 +125,15 @@
</div>
</div>
<div class="modal" *sqxModalView="addFieldDialog;onRoot:true;closeAuto:false" @fade>
<div class="modal-backdrop"></div>
<sqx-field-wizard
(completed)="addFieldDialog.hide()" [schema]="schema">
</sqx-field-wizard>
</div>
<div class="modal" *sqxModalView="configureScriptsDialog;onRoot:true" @fade>
<div class="modal-backdrop"></div>
<div class="modal-dialog modal-lg">

21
src/Squidex/app/features/schemas/pages/schema/schema-page.component.scss

@ -36,9 +36,24 @@
color: $color-border-dark;
}
.form-check {
margin-top: .4rem;
margin-bottom: -.2rem;
.field-button {
& {
@include circle(5.25rem);
@include box-shadow(0, 8px, 16px, .3);
@include absolute(auto, 6.5rem, 1rem, auto);
}
&-icon {
font-weight: bold;
}
&-text {
font-size: .9rem;
}
}
.panel-content-scroll {
padding-bottom: 7rem;
}
.dnd-sortable-drag {

242
src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts

@ -6,40 +6,24 @@
*/
import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { Subscription } from 'rxjs';
import {
AddFieldDto,
AppContext,
AppPatternDto,
AppPatternsService,
createProperties,
fadeAnimation,
FieldDto,
FieldPropertiesDto,
fieldTypes,
HistoryChannelUpdated,
ImmutableArray,
ModalView,
SchemaDetailsDto,
SchemaDto,
SchemaPropertiesDto,
SchemasService,
UpdateFieldDto,
UpdateSchemaScriptsDto,
ValidatorsEx,
Version
SchemaDetailsDto
} from 'shared';
import {
SchemaCloning,
SchemaCreated,
SchemaDeleted,
SchemaUpdated
SchemaCloning
} from './../messages';
import { SchemasState } from './../../state/schemas.state';
@Component({
selector: 'sqx-schema-page',
styleUrls: ['./schema-page.component.scss'],
@ -58,9 +42,6 @@ export class SchemaPageComponent implements OnDestroy, OnInit {
public schemaExport: any;
public schema: SchemaDetailsDto;
public schemas: ImmutableArray<SchemaDto>;
public regexSuggestions: AppPatternDto[] = [];
public exportSchemaDialog = new ModalView();
@ -69,30 +50,11 @@ export class SchemaPageComponent implements OnDestroy, OnInit {
public editOptionsDropdown = new ModalView();
public editSchemaDialog = new ModalView();
public addFieldFormSubmitted = false;
public addFieldForm =
this.formBuilder.group({
type: ['String',
[
Validators.required
]],
name: ['',
[
Validators.maxLength(40),
ValidatorsEx.pattern('[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*', 'Name must be a valid javascript name in camel case.')
]],
isLocalizable: [false]
});
public get hasName() {
return this.addFieldForm.controls['name'].value && this.addFieldForm.controls['name'].value.length > 0;
}
public addFieldDialog = new ModalView();
constructor(public readonly ctx: AppContext,
private readonly formBuilder: FormBuilder,
private readonly router: Router,
private readonly schemasService: SchemasService,
private readonly appPatternsService: AppPatternsService
private readonly schemasState: SchemasState
) {
}
@ -101,203 +63,37 @@ export class SchemaPageComponent implements OnDestroy, OnInit {
}
public ngOnInit() {
this.schemaCreatedSubscription =
this.ctx.bus.of(SchemaCreated)
.subscribe(message => {
if (this.schemas) {
this.schemas = this.schemas.push(message.schema);
}
});
this.ctx.route.data.map(d => d.schema)
.subscribe((schema: SchemaDetailsDto) => {
this.schema = schema;
this.schemasState.selectedSchema
.subscribe(schema => {
this.schema = schema!;
this.export();
});
this.load();
}
private load() {
this.appPatternsService.getPatterns(this.ctx.appName)
.subscribe(dtos => {
this.regexSuggestions = dtos.patterns;
});
this.schemasService.getSchemas(this.ctx.appName)
.subscribe(dtos => {
this.schemas = ImmutableArray.of(dtos);
}, error => {
this.ctx.notifyError(error);
});
}
public publish() {
this.schemasService.publishSchema(this.ctx.appName, this.schema.name, this.schema.version)
.subscribe(dto => {
this.updateSchema(this.schema.publish(this.ctx.userToken, dto.version));
}, error => {
this.ctx.notifyError(error);
});
this.schemasState.publish(this.schema).subscribe();
}
public unpublish() {
this.schemasService.unpublishSchema(this.ctx.appName, this.schema.name, this.schema.version)
.subscribe(dto => {
this.updateSchema(this.schema.unpublish(this.ctx.userToken, dto.version));
}, error => {
this.ctx.notifyError(error);
});
}
public enableField(field: FieldDto) {
this.schemasService.enableField(this.ctx.appName, this.schema.name, field.fieldId, this.schema.version)
.subscribe(dto => {
this.updateSchema(this.schema.updateField(field.enable(), this.ctx.userToken, dto.version));
}, error => {
this.ctx.notifyError(error);
});
}
public disableField(field: FieldDto) {
this.schemasService.disableField(this.ctx.appName, this.schema.name, field.fieldId, this.schema.version)
.subscribe(dto => {
this.updateSchema(this.schema.updateField(field.disable(), this.ctx.userToken, dto.version));
}, error => {
this.ctx.notifyError(error);
});
}
public lockField(field: FieldDto) {
this.schemasService.lockField(this.ctx.appName, this.schema.name, field.fieldId, this.schema.version)
.subscribe(dto => {
this.updateSchema(this.schema.updateField(field.lock(), this.ctx.userToken, dto.version));
}, error => {
this.ctx.notifyError(error);
});
}
public showField(field: FieldDto) {
this.schemasService.showField(this.ctx.appName, this.schema.name, field.fieldId, this.schema.version)
.subscribe(dto => {
this.updateSchema(this.schema.updateField(field.show(), this.ctx.userToken, dto.version));
}, error => {
this.ctx.notifyError(error);
});
}
public hideField(field: FieldDto) {
this.schemasService.hideField(this.ctx.appName, this.schema.name, field.fieldId, this.schema.version)
.subscribe(dto => {
this.updateSchema(this.schema.updateField(field.hide(), this.ctx.userToken, dto.version));
}, error => {
this.ctx.notifyError(error);
});
}
public deleteField(field: FieldDto) {
this.schemasService.deleteField(this.ctx.appName, this.schema.name, field.fieldId, this.schema.version)
.subscribe(dto => {
this.updateSchema(this.schema.removeField(field, this.ctx.userToken, dto.version));
}, error => {
this.ctx.notifyError(error);
});
this.schemasState.unpublish(this.schema).subscribe();
}
public sortFields(fields: FieldDto[]) {
this.schemasService.putFieldOrdering(this.ctx.appName, this.schema.name, fields.map(t => t.fieldId), this.schema.version)
.subscribe(dto => {
this.updateSchema(this.schema.replaceFields(fields, this.ctx.userToken, dto.version));
}, error => {
this.ctx.notifyError(error);
});
}
public saveField(field: FieldDto, properties: FieldPropertiesDto) {
const requestDto = new UpdateFieldDto(properties);
this.schemasService.putField(this.ctx.appName, this.schema.name, field.fieldId, requestDto, this.schema.version)
.subscribe(dto => {
this.updateSchema(this.schema.updateField(field.update(properties), this.ctx.userToken, dto.version));
}, error => {
this.ctx.notifyError(error);
});
this.schemasState.sortFields(this.schema, fields).subscribe();
}
public deleteSchema() {
this.schemasService.deleteSchema(this.ctx.appName, this.schema.name, this.schema.version)
this.schemasState.delete(this.schema)
.subscribe(() => {
this.onSchemaRemoved(this.schema);
this.back();
}, error => {
this.ctx.notifyError(error);
});
}
public addField() {
this.addFieldFormSubmitted = true;
if (this.addFieldForm.valid) {
this.addFieldForm.disable();
const properties = createProperties(this.addFieldForm.controls['type'].value);
const partitioning = this.addFieldForm.controls['isLocalizable'].value ? 'language' : 'invariant';
const requestDto = new AddFieldDto(this.addFieldForm.controls['name'].value, partitioning, properties);
this.schemasService.postField(this.ctx.appName, this.schema.name, requestDto, this.schema.version)
.subscribe(dto => {
this.updateSchema(this.schema.addField(dto.payload, this.ctx.userToken, dto.version));
this.resetFieldForm();
}, error => {
this.ctx.notifyError(error);
this.resetFieldForm();
});
}
}
public cancelAddField() {
this.resetFieldForm();
}
public cloneSchema() {
this.ctx.bus.emit(new SchemaCloning(this.schemaExport));
}
public onSchemaSaved(properties: SchemaPropertiesDto, version: Version) {
this.updateSchema(this.schema.update(properties, this.ctx.userToken, version));
this.editSchemaDialog.hide();
}
public onSchemaScriptsSaved(scripts: UpdateSchemaScriptsDto, version: Version) {
this.updateSchema(this.schema.configureScripts(scripts, this.ctx.userToken, version));
this.configureScriptsDialog.hide();
}
private onSchemaRemoved(schema: SchemaDto) {
this.schemas = this.schemas.removeAll(s => s.id === schema.id);
this.emitSchemaDeleted(schema);
}
private resetFieldForm() {
this.addFieldForm.enable();
this.addFieldForm.reset({ type: 'String' });
this.addFieldFormSubmitted = false;
}
private updateSchema(schema: SchemaDetailsDto) {
this.schema = schema;
this.schemas = this.schemas.replaceBy('id', schema);
this.emitSchemaUpdated(schema);
this.emitHistoryUpdate();
this.export();
}
private export() {
const result: any = {
fields: this.schema.fields.map(field => {
@ -332,17 +128,5 @@ export class SchemaPageComponent implements OnDestroy, OnInit {
private back() {
this.router.navigate(['../'], { relativeTo: this.ctx.route });
}
private emitSchemaDeleted(schema: SchemaDto) {
this.ctx.bus.emit(new SchemaDeleted(schema));
}
private emitSchemaUpdated(schema: SchemaDto) {
this.ctx.bus.emit(new SchemaUpdated(schema));
}
private emitHistoryUpdate() {
this.ctx.bus.emit(new HistoryChannelUpdated());
}
}

31
src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts

@ -11,13 +11,11 @@ import { FormBuilder, Validators } from '@angular/forms';
import {
ApiUrlConfig,
AppContext,
DateTime,
fadeAnimation,
SchemaDetailsDto,
SchemasService,
ValidatorsEx
} from 'shared';
import { SchemasState } from './../../state/schemas.state';
const FALLBACK_NAME = 'my-schema';
@Component({
@ -26,17 +24,11 @@ const FALLBACK_NAME = 'my-schema';
templateUrl: './schema-form.component.html',
providers: [
AppContext
],
animations: [
fadeAnimation
]
})
export class SchemaFormComponent implements OnInit {
@Output()
public created = new EventEmitter<SchemaDetailsDto>();
@Output()
public cancelled = new EventEmitter();
public completed = new EventEmitter();
@Input()
public import: any;
@ -63,7 +55,7 @@ export class SchemaFormComponent implements OnInit {
constructor(
public readonly apiUrl: ApiUrlConfig,
public readonly ctx: AppContext,
private readonly schemas: SchemasService,
private readonly schemasState: SchemasState,
private readonly formBuilder: FormBuilder
) {
}
@ -81,7 +73,7 @@ export class SchemaFormComponent implements OnInit {
}
public cancel() {
this.emitCancelled();
this.emitCompleted();
this.resetCreateForm();
}
@ -94,9 +86,10 @@ export class SchemaFormComponent implements OnInit {
const schemaName = this.createForm.controls['name'].value;
const schemaDto = Object.assign(this.createForm.controls['import'].value || {}, { name: schemaName });
this.schemas.postSchema(this.ctx.appName, schemaDto, this.ctx.userToken, DateTime.now())
this.schemasState.create(schemaDto)
.subscribe(dto => {
this.emitCreated(dto);
this.emitCompleted();
this.resetCreateForm();
}, error => {
this.enableCreateForm(error.displayMessage);
@ -104,12 +97,8 @@ export class SchemaFormComponent implements OnInit {
}
}
private emitCancelled() {
this.cancelled.emit();
}
private emitCreated(schema: SchemaDetailsDto) {
this.created.emit(schema);
private emitCompleted() {
this.completed.emit();
}
private enableCreateForm(message: string) {

7
src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html

@ -29,7 +29,7 @@
<div class="panel-main">
<div class="panel-content">
<ul class="nav nav-panel nav-dark nav-dark-bordered flex-column">
<li class="nav-item" *ngFor="let schema of schemasFiltered">
<li class="nav-item" *ngFor="let schema of schemasFiltered | async; trackBy: schemasState.trackBy">
<a class="nav-link" [routerLink]="[schema.name]" routerLinkActive="active">
<div class="row">
<div class="col col-4">
@ -67,10 +67,7 @@
</div>
<div class="modal-body">
<sqx-schema-form
[import]="import"
(created)="onSchemaCreated($event)"
(cancelled)="addSchemaDialog.hide()">
<sqx-schema-form [import]="import" (completed)="addSchemaDialog.hide()">
</sqx-schema-form>
</div>
</div>

105
src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts

@ -5,26 +5,16 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Subscription } from 'rxjs';
import {
AppContext,
fadeAnimation,
ImmutableArray,
ModalView,
SchemaDto,
SchemasService
ModalView
} from 'shared';
import {
SchemaCloning,
SchemaCreated,
SchemaDeleted,
SchemaUpdated
} from './../messages';
import { SchemasState } from './../../state/schemas.state';
@Component({
selector: 'sqx-schemas-page',
@ -37,40 +27,29 @@ import {
fadeAnimation
]
})
export class SchemasPageComponent implements OnDestroy, OnInit {
private schemaUpdatedSubscription: Subscription;
private schemaDeletedSubscription: Subscription;
private schemaCloningSubscription: Subscription;
export class SchemasPageComponent implements OnInit {
public addSchemaDialog = new ModalView();
public schemas = ImmutableArray.empty<SchemaDto>();
public schemaQuery: string;
public schemasFilter = new FormControl();
public schemasFiltered = ImmutableArray.empty<SchemaDto>();
public schemasFiltered =
this.schemasState.schemasItems
.combineLatest(this.schemasFilter.valueChanges.startWith(''),
(schemas, query) => {
if (query && query.length > 0) {
schemas = schemas.filter(t => t.name.indexOf(query) >= 0);
}
return schemas.sortByStringAsc(x => x.name);
});
public import: any;
constructor(public readonly ctx: AppContext,
private readonly router: Router,
private readonly schemasService: SchemasService
private readonly schemasState: SchemasState
) {
}
public ngOnDestroy() {
this.schemaUpdatedSubscription.unsubscribe();
this.schemaDeletedSubscription.unsubscribe();
this.schemaCloningSubscription.unsubscribe();
}
public ngOnInit() {
this.schemasFilter.valueChanges
.distinctUntilChanged()
.debounceTime(100)
.subscribe(q => {
this.updateSchemas(this.schemas, this.schemaQuery = q);
});
this.ctx.route.params.map(q => q['showDialog'])
.subscribe(showDialog => {
if (showDialog) {
@ -78,25 +57,7 @@ export class SchemasPageComponent implements OnDestroy, OnInit {
}
});
this.schemaCloningSubscription =
this.ctx.bus.of(SchemaCloning)
.subscribe(m => {
this.createSchema(m.importing);
});
this.schemaUpdatedSubscription =
this.ctx.bus.of(SchemaUpdated)
.subscribe(m => {
this.updateSchemas(this.schemas.replaceBy('id', m.schema));
});
this.schemaDeletedSubscription =
this.ctx.bus.of(SchemaDeleted)
.subscribe(m => {
this.updateSchemas(this.schemas.filter(s => s.id !== m.schema.id));
});
this.load();
this.schemasState.load().subscribe();
}
public createSchema(importing: any) {
@ -104,39 +65,5 @@ export class SchemasPageComponent implements OnDestroy, OnInit {
this.addSchemaDialog.show();
}
private load() {
this.schemasService.getSchemas(this.ctx.appName)
.subscribe(dtos => {
this.updateSchemas(ImmutableArray.of(dtos));
}, error => {
this.ctx.notifyError(error);
});
}
public onSchemaCreated(schema: SchemaDto) {
this.updateSchemas(this.schemas.push(schema), this.schemaQuery);
this.emitSchemaCreated(schema);
this.addSchemaDialog.hide();
this.router.navigate([ schema.name ], { relativeTo: this.ctx.route });
}
private emitSchemaCreated(schema: SchemaDto) {
this.ctx.bus.emit(new SchemaCreated(schema));
}
private updateSchemas(schemas: ImmutableArray<SchemaDto>, query?: string) {
this.schemas = schemas;
query = query || this.schemaQuery;
if (query && query.length > 0) {
schemas = schemas.filter(t => t.name.indexOf(query!) >= 0);
}
this.schemasFiltered = schemas.sortByStringAsc(x => x.name);
}
}

201
src/Squidex/app/features/schemas/state/schemas.state.ts

@ -0,0 +1,201 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import 'framework/utils/rxjs-extensions';
import {
AddFieldDto,
AppsStoreService,
AuthService,
CreateSchemaDto,
DateTime,
DialogService,
ErrorDto,
FieldDto,
ImmutableArray,
Notification,
SchemaDto,
SchemaDetailsDto,
SchemasService,
UpdateFieldDto
} from 'shared';
@Injectable()
export class SchemasState {
public schemasItems = new BehaviorSubject<ImmutableArray<SchemaDto>>(ImmutableArray.empty());
public selectedSchema = new BehaviorSubject<SchemaDetailsDto | null>(null);
private get app() {
return this.appsState.app$.value!.name;
}
private get user() {
return this.authState.user!.token;
}
constructor(
private readonly schemasService: SchemasService,
private readonly dialogs: DialogService,
private readonly authState: AuthService,
private readonly appsState: AppsStoreService
) {
}
public selectSchema(id: string | null): Observable<boolean> {
const observable =
!id ?
Observable.of(null) :
Observable.of(<SchemaDetailsDto>this.schemasItems.value.find(x => x.id === id && x instanceof SchemaDetailsDto))
.switchMap(schema => {
if (!schema) {
return this.schemasService.getSchema(this.appsState.app$.value!.name, id).catch(() => Observable.of(null));
} else {
return Observable.of(schema);
}
});
return observable
.do(schema => {
this.selectedSchema.next(schema);
})
.map(s => s !== null);
}
public load(): Observable<any> {
return this.schemasService.getSchemas(this.app)
.catch(error => this.notifyError(error))
.do(dtos => {
this.schemasItems.nextBy(v => ImmutableArray.of(dtos));
});
}
public create(request: CreateSchemaDto) {
return this.schemasService.postSchema(this.app, request, this.user, DateTime.now())
.do(dto => {
this.schemasItems.nextBy(v => v.push(dto));
});
}
public addField(schema: SchemaDetailsDto, request: AddFieldDto): Observable<FieldDto> {
return this.schemasService.postField(this.app, schema.name, request, schema.version)
.do(dto => {
this.replaceSchema(schema.addField(dto.payload, this.user, dto.version));
}).map(d => d.payload);
}
public publish(schema: SchemaDto): Observable<any> {
return this.schemasService.publishSchema(this.app, schema.name, schema.version)
.catch(error => this.notifyError(error))
.do(dto => {
this.replaceSchema(schema.publish(this.user, dto.version));
});
}
public unpublish(schema: SchemaDto): Observable<any> {
return this.schemasService.unpublishSchema(this.app, schema.name, schema.version)
.catch(error => this.notifyError(error))
.do(dto => {
this.replaceSchema(schema.unpublish(this.user, dto.version));
});
}
public enableField(schema: SchemaDetailsDto, field: FieldDto): Observable<any> {
return this.schemasService.enableField(this.app, schema.name, field.fieldId, schema.version)
.catch(error => this.notifyError(error))
.do(dto => {
this.replaceSchema(schema.updateField(field.enable(), this.user, dto.version));
});
}
public disableField(schema: SchemaDetailsDto, field: FieldDto): Observable<any> {
return this.schemasService.disableField(this.app, schema.name, field.fieldId, schema.version)
.catch(error => this.notifyError(error))
.do(dto => {
this.replaceSchema(schema.updateField(field.disable(), this.user, dto.version));
});
}
public lockField(schema: SchemaDetailsDto, field: FieldDto): Observable<any> {
return this.schemasService.lockField(this.app, schema.name, field.fieldId, schema.version)
.catch(error => this.notifyError(error))
.do(dto => {
this.replaceSchema(schema.updateField(field.lock(), this.user, dto.version));
});
}
public showField(schema: SchemaDetailsDto, field: FieldDto): Observable<any> {
return this.schemasService.showField(this.app, schema.name, field.fieldId, schema.version)
.catch(error => this.notifyError(error))
.do(dto => {
this.replaceSchema(schema.updateField(field.show(), this.user, dto.version));
});
}
public hideField(schema: SchemaDetailsDto, field: FieldDto): Observable<any> {
return this.schemasService.hideField(this.app, schema.name, field.fieldId, schema.version)
.catch(error => this.notifyError(error))
.do(dto => {
this.replaceSchema(schema.updateField(field.hide(), this.user, dto.version));
});
}
public deleteField(schema: SchemaDetailsDto, field: FieldDto): Observable<any> {
return this.schemasService.deleteField(this.app, schema.name, field.fieldId, schema.version)
.catch(error => this.notifyError(error))
.do(dto => {
this.replaceSchema(schema.removeField(field, this.user, dto.version));
});
}
public sortFields(schema: SchemaDetailsDto, fields: FieldDto[]): Observable<any> {
return this.schemasService.putFieldOrdering(this.app, schema.name, fields.map(t => t.fieldId), schema.version)
.catch(error => this.notifyError(error))
.do(dto => {
this.replaceSchema(schema.replaceFields(fields, this.user, dto.version));
});
}
public updateField(schema: SchemaDetailsDto, field: FieldDto, request: UpdateFieldDto): Observable<any> {
return this.schemasService.putField(this.app, schema.name, field.fieldId, request, schema.version)
.catch(error => this.notifyError(error))
.do(dto => {
this.replaceSchema(schema.updateField(field.update(request.properties), this.user, dto.version));
});
}
public delete(schema: SchemaDto): Observable<any> {
return this.schemasService.deleteSchema(this.app, schema.name, schema.version)
.catch(error => this.notifyError(error))
.do(dto => {
this.schemasItems.nextBy(v => v.filter(s => s.id !== schema.id));
});
}
private replaceSchema(schema: SchemaDto) {
this.schemasItems.nextBy(v => v.replaceBy('id', schema));
this.selectedSchema.nextBy(v => v !== null && v.id === schema.id ? <SchemaDetailsDto>schema : v);
}
public trackBy(index: number, schema: SchemaDto): any {
return schema.id;
}
private notifyError(error: string | ErrorDto) {
if (error instanceof ErrorDto) {
this.dialogs.notify(Notification.error(error.displayMessage));
} else {
this.dialogs.notify(Notification.error(error));
}
return Observable.throw(error);
}
}

2
src/Squidex/app/shared/declarations-base.ts

@ -38,6 +38,8 @@ export * from './services/usages.service';
export * from './services/users-provider.service';
export * from './services/users.service';
export * from './state/apps.state';
export * from './utils/messages';
export * from 'framework';

4
src/Squidex/app/shared/services/apps-store.service.ts

@ -18,8 +18,8 @@ import {
@Injectable()
export class AppsStoreService {
private readonly apps$ = new ReplaySubject<AppDto[]>(1);
private readonly app$ = new BehaviorSubject<AppDto | null>(null);
public readonly apps$ = new ReplaySubject<AppDto[]>(1);
public readonly app$ = new BehaviorSubject<AppDto | null>(null);
public get apps(): Observable<AppDto[]> {
return this.apps$;

39
src/Squidex/app/shared/services/schemas.service.ts

@ -23,16 +23,35 @@ import {
Versioned
} from 'framework';
export const fieldTypes: string[] = [
'Assets',
'Boolean',
'DateTime',
'Geolocation',
'Json',
'Number',
'References',
'String',
'Tags'
export const fieldTypes = [
{
type: 'String',
description: 'Titles, names, paragraphs.'
}, {
type: 'Assets',
description: 'Images, videos, documents.'
}, {
type: 'Boolean',
description: 'Yes or no, true or false, 1 or 0.'
}, {
type: 'DateTime',
description: 'Events date, opening hours.'
}, {
type: 'Geolocation',
description: 'Coordinates: latitude and longitude.'
}, {
type: 'Json',
description: 'Data in JSON format, for developers.'
}, {
type: 'Number',
description: 'ID, order number, rating, quantity.'
}, {
type: 'References',
description: 'Links to other content items.'
}, {
type: 'Tags',
description: 'Special format for tags.'
}
];
export const fieldInvariant = 'iv';

65
src/Squidex/app/shared/state/apps.state.ts

@ -0,0 +1,65 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import 'framework/utils/rxjs-extensions';
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { DateTime, ImmutableArray } from 'framework';
import {
AppDto,
AppsService,
CreateAppDto
} from './../services/apps.service';
@Injectable()
export class AppsState {
public apps = new BehaviorSubject<ImmutableArray<AppDto>>(ImmutableArray.empty());
public selectedApp = new BehaviorSubject<AppDto | null>(null);
constructor(
private readonly appsService: AppsService
) {
}
public loadApps(): Observable<any> {
return this.appsService.getApps()
.do(dtos => {
this.apps.nextBy(v => ImmutableArray.of(dtos));
});
}
public selectApp(name: string | null): Observable<AppDto | null> {
const observable =
!name ?
Observable.of(null) :
Observable.of(this.apps.value.find(x => x.name === name) || null);
return observable
.do(app => {
this.selectedApp.next(app);
});
}
public createApp(dto: CreateAppDto, now?: DateTime): Observable<AppDto> {
return this.appsService.postApp(dto)
.do(app => {
this.apps.nextBy(v => v.push(app));
});
}
public deleteApp(name: string): Observable<any> {
return this.appsService.deleteApp(name)
.do(app => {
this.apps.nextBy(v => v.filter(a => a.name !== name));
});
}
}

1
src/Squidex/app/shared/utils/messages.ts

@ -18,7 +18,6 @@ export class AssetUpdated {
}
export class AssetDragged {
public static readonly DRAG_START = 'Start';
public static readonly DRAG_END = 'End';

1
src/Squidex/app/theme/_bootstrap.scss

@ -303,6 +303,7 @@ a {
// Special radio button.
&-radio {
& {
@include border-radius;
color: $color-border-dark;
cursor: pointer;
border: 1px solid $color-border;

2
src/Squidex/app/theme/_rules.scss

@ -27,7 +27,7 @@ $action-fastly: #e23335;
}
}
.rule-title {
.wizard-title {
margin-bottom: 1rem;
}

17
src/Squidex/app/theme/icomoon/icons/type-Tags.svg

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 17.3 17.3" style="enable-background:new 0 0 17.3 17.3;" xml:space="preserve">
<style type="text/css">
.st0{fill:#C1C6C8;}
.st1{fill:none;stroke:#C1C6C8;stroke-miterlimit:10;}
</style>
<g>
<path class="st0" d="M5,13.9H3.4c-0.8,0-1.5-0.7-1.5-1.5V4.1c0-0.8,0.7-1.5,1.5-1.5l1.6,0v-1H3.4C2,1.6,0.9,2.7,0.9,4.1v8.3
c0,1.4,1.1,2.5,2.5,2.5H5V13.9z"/>
<path class="st0" d="M16.4,8C15,5.1,13,1.6,11.8,1.6H9.9v1l1.9,0c0.6,0.2,2.3,3,3.6,5.7c-1.3,2.6-3,5.5-3.6,5.7H9.9v1h1.9
c1.2,0,3.1-3.5,4.6-6.5l0.1-0.2L16.4,8z"/>
</g>
<line class="st1" x1="5" y1="5.7" x2="5" y2="10.1"/>
<line class="st1" x1="8.6" y1="5.7" x2="8.6" y2="10.1"/>
</svg>

After

Width:  |  Height:  |  Size: 909 B

Loading…
Cancel
Save