Browse Source

Cleanup

pull/1/head
Sebastian 9 years ago
parent
commit
122cad7586
  1. 2
      src/Squidex.Core/Contents/ContentData.cs
  2. 12
      src/Squidex.Core/Schemas/FieldProperties.cs
  3. 2
      src/Squidex.Core/Schemas/Schema.cs
  4. 2
      src/Squidex.Read/Contents/IContentEntity.cs
  5. 3
      src/Squidex.Read/Schemas/ISchemaEntity.cs
  6. 2
      src/Squidex.Read/Schemas/ISchemaEntityWithSchema.cs
  7. 1
      src/Squidex.Read/Schemas/Services/ISchemaProvider.cs
  8. 6
      src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs
  9. 14
      src/Squidex.Store.MongoDb/Contents/MongoContentEntity.cs
  10. 2
      src/Squidex.Store.MongoDb/Schemas/MongoSchemaEntity.cs
  11. 1
      src/Squidex.Store.MongoDb/Schemas/MongoSchemaRepository.cs
  12. 2
      src/Squidex/Controllers/Api/Schemas/Models/Converters/SchemaConverter.cs
  13. 10
      src/Squidex/Controllers/Api/Schemas/Models/FieldPropertiesDto.cs
  14. 2
      src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs
  15. 1
      src/Squidex/Properties/launchSettings.json
  16. 1
      src/Squidex/app/features/content/declarations.ts
  17. 9
      src/Squidex/app/features/content/module.ts
  18. 86
      src/Squidex/app/features/content/pages/content/content-field.component.html
  19. 22
      src/Squidex/app/features/content/pages/content/content-field.component.scss
  20. 56
      src/Squidex/app/features/content/pages/content/content-field.component.ts
  21. 86
      src/Squidex/app/features/content/pages/content/content-page.component.html
  22. 4
      src/Squidex/app/features/content/pages/content/content-page.component.scss
  23. 49
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  24. 75
      src/Squidex/app/features/content/pages/contents/contents-page.component.html
  25. 22
      src/Squidex/app/features/content/pages/contents/contents-page.component.scss
  26. 83
      src/Squidex/app/features/content/pages/contents/contents-page.component.ts
  27. 8
      src/Squidex/app/features/content/pages/messages.ts
  28. 4
      src/Squidex/app/features/content/pages/schemas/schemas-page.component.html
  29. 13
      src/Squidex/app/features/content/pages/schemas/schemas-page.component.scss
  30. 16
      src/Squidex/app/features/schemas/pages/schema/field.component.html
  31. 4
      src/Squidex/app/features/schemas/pages/schema/field.component.scss
  32. 4
      src/Squidex/app/features/schemas/pages/schema/field.component.ts
  33. 6
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.html
  34. 5
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html
  35. 31
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.scss
  36. 2
      src/Squidex/app/features/settings/pages/clients/client.component.html
  37. 2
      src/Squidex/app/features/settings/pages/clients/client.component.scss
  38. 10
      src/Squidex/app/features/settings/pages/clients/clients-page.component.html
  39. 5
      src/Squidex/app/features/settings/pages/clients/clients-page.component.scss
  40. 12
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html
  41. 10
      src/Squidex/app/features/settings/pages/languages/languages-page.component.html
  42. 10
      src/Squidex/app/features/settings/settings-area.component.html
  43. 8
      src/Squidex/app/framework/angular/control-errors.component.ts
  44. 28
      src/Squidex/app/shared/app-component-base.ts
  45. 12
      src/Squidex/app/shared/components/history.component.html
  46. 1
      src/Squidex/app/shared/declarations.ts
  47. 83
      src/Squidex/app/shared/guards/resolve-app-languages.guard.spec.ts
  48. 62
      src/Squidex/app/shared/guards/resolve-app-languages.guard.ts
  49. 2
      src/Squidex/app/shared/module.ts
  50. 49
      src/Squidex/app/shared/services/contents.service.spec.ts
  51. 16
      src/Squidex/app/shared/services/contents.service.ts
  52. 31
      src/Squidex/app/shared/services/schemas.service.ts
  53. 19
      src/Squidex/app/theme/_panels.scss
  54. 2
      src/Squidex/app/theme/_vars.scss
  55. 4
      src/Squidex/app/theme/_vendor-overrides.scss
  56. 31
      tests/Squidex.Core.Tests/Contents/ContentDataTests.cs
  57. 2
      tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs
  58. 2
      tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs

2
src/Squidex.Core/Contents/ContentData.cs

@ -62,7 +62,7 @@ namespace Squidex.Core.Contents
{
Field field;
if (!schema.FieldsByName.TryGetValue(fieldValue.Key, out field))
if (!schema.FieldsByName.TryGetValue(fieldValue.Key, out field) || field.IsHidden)
{
continue;
}

12
src/Squidex.Core/Schemas/FieldProperties.cs

@ -15,6 +15,7 @@ namespace Squidex.Core.Schemas
{
private bool isRequired;
private bool isLocalizable;
private bool isListField;
private string placeholder;
public bool IsRequired
@ -39,6 +40,17 @@ namespace Squidex.Core.Schemas
}
}
public bool IsListField
{
get { return isListField; }
set
{
ThrowIfFrozen();
isListField = value;
}
}
public string Placeholder
{
get { return placeholder; }

2
src/Squidex.Core/Schemas/Schema.cs

@ -180,7 +180,7 @@ namespace Squidex.Core.Schemas
var schema = new JsonSchema4 { Id = Name, Type = JsonObjectType.Object };
foreach (var field in fieldsByName.Values)
foreach (var field in fieldsByName.Values.Where(x => !x.IsHidden))
{
field.AddToSchema(schema, languages, Name, schemaResolver);
}

2
src/Squidex.Read/Contents/IContentEntity.cs

@ -10,7 +10,7 @@ using Squidex.Core.Contents;
namespace Squidex.Read.Contents
{
public interface IContentEntity : IEntity
public interface IContentEntity : IAppRefEntity, ITrackCreatedByEntity, ITrackLastModifiedByEntity
{
bool IsPublished { get; }

3
src/Squidex.Read/Schemas/Repositories/ISchemaEntity.cs → src/Squidex.Read/Schemas/ISchemaEntity.cs

@ -5,7 +5,8 @@
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Read.Schemas.Repositories
namespace Squidex.Read.Schemas
{
public interface ISchemaEntity : IAppRefEntity, ITrackCreatedByEntity, ITrackLastModifiedByEntity
{

2
src/Squidex.Read/Schemas/Repositories/ISchemaEntityWithSchema.cs → src/Squidex.Read/Schemas/ISchemaEntityWithSchema.cs

@ -8,7 +8,7 @@
using Squidex.Core.Schemas;
namespace Squidex.Read.Schemas.Repositories
namespace Squidex.Read.Schemas
{
public interface ISchemaEntityWithSchema : ISchemaEntity
{

1
src/Squidex.Read/Schemas/Services/ISchemaProvider.cs

@ -8,7 +8,6 @@
using System;
using System.Threading.Tasks;
using Squidex.Read.Schemas.Repositories;
namespace Squidex.Read.Schemas.Services
{

6
src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs

@ -89,7 +89,11 @@ namespace Squidex.Read.Schemas.Services.Implementations
public Task On(Envelope<IEvent> @event)
{
if (@event.Payload is SchemaDeleted)
if (@event.Payload is SchemaDeleted ||
@event.Payload is SchemaPublished ||
@event.Payload is SchemaUnpublished ||
@event.Payload is SchemaUpdated ||
@event.Payload is FieldEvent)
{
var cacheKey = BuildIdCacheKey(@event.Headers.AggregateId());

14
src/Squidex.Store.MongoDb/Contents/MongoContentEntity.cs

@ -6,6 +6,7 @@
// All rights reserved.
// ==========================================================================
using System;
using System.Linq;
using System.Text;
using MongoDB.Bson;
@ -13,6 +14,7 @@ using MongoDB.Bson.Serialization.Attributes;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Squidex.Core.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb;
using Squidex.Read.Contents;
// ReSharper disable InvertIf
@ -36,6 +38,18 @@ namespace Squidex.Store.MongoDb.Contents
[BsonElement]
public string Text { get; set; }
[BsonRequired]
[BsonElement]
public Guid AppId { get; set; }
[BsonRequired]
[BsonElement]
public RefToken CreatedBy { get; set; }
[BsonRequired]
[BsonElement]
public RefToken LastModifiedBy { get; set; }
[BsonRequired]
[BsonElement]
public BsonDocument Data

2
src/Squidex.Store.MongoDb/Schemas/MongoSchemaEntity.cs

@ -13,7 +13,7 @@ using Squidex.Core.Schemas;
using Squidex.Core.Schemas.Json;
using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb;
using Squidex.Read.Schemas.Repositories;
using Squidex.Read.Schemas;
namespace Squidex.Store.MongoDb.Schemas
{

1
src/Squidex.Store.MongoDb/Schemas/MongoSchemaRepository.cs

@ -22,6 +22,7 @@ using Squidex.Infrastructure.CQRS.Replay;
using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Reflection;
using Squidex.Read.Schemas;
using Squidex.Read.Schemas.Repositories;
using Squidex.Store.MongoDb.Utils;

2
src/Squidex/Controllers/Api/Schemas/Models/Converters/SchemaConverter.cs

@ -11,7 +11,7 @@ using System.Collections.Generic;
using System.Linq;
using Squidex.Core.Schemas;
using Squidex.Infrastructure.Reflection;
using Squidex.Read.Schemas.Repositories;
using Squidex.Read.Schemas;
namespace Squidex.Controllers.Api.Schemas.Models.Converters
{

10
src/Squidex/Controllers/Api/Schemas/Models/FieldPropertiesDto.cs

@ -43,6 +43,16 @@ namespace Squidex.Controllers.Api.Schemas.Models
/// </summary>
public bool IsRequired { get; set; }
/// <summary>
/// Determines if the field should be displayed in lists.
/// </summary>
public bool IsListField { get; set; }
/// <summary>
/// Determines if the field is localizable.
/// </summary>
public bool IsLocalizable { get; set; }
public abstract FieldProperties ToProperties();
}
}

2
src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs

@ -23,7 +23,7 @@ using Squidex.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Pipeline.Swagger;
using Squidex.Read.Apps;
using Squidex.Read.Schemas.Repositories;
using Squidex.Read.Schemas;
// ReSharper disable SuggestBaseTypeForParameter
// ReSharper disable PrivateFieldCanBeConvertedToLocalVariable

1
src/Squidex/Properties/launchSettings.json

@ -16,6 +16,7 @@
},
"Squidex": {
"commandName": "Project",
"commandLineArgs": "--replay",
"launchUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"

1
src/Squidex/app/features/content/declarations.ts

@ -5,6 +5,7 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
export * from './pages/content/content-field.component';
export * from './pages/content/content-page.component';
export * from './pages/contents/contents-page.component';
export * from './pages/schemas/schemas-page.component';

9
src/Squidex/app/features/content/module.ts

@ -10,12 +10,14 @@ import { RouterModule, Routes } from '@angular/router';
import {
HistoryComponent,
ResolveAppLanguagesGuard,
ResolvePublishedSchemaGuard,
SqxFrameworkModule,
SqxSharedModule
} from 'shared';
import {
ContentFieldComponent,
ContentPageComponent,
ContentsPageComponent,
SchemasPageComponent
@ -33,14 +35,14 @@ const routes: Routes = [
path: ':schemaName',
component: ContentsPageComponent,
resolve: {
schema: ResolvePublishedSchemaGuard
schema: ResolvePublishedSchemaGuard, appLanguages: ResolveAppLanguagesGuard
},
children: [
{
path: 'new',
component: ContentPageComponent,
resolve: {
schema: ResolvePublishedSchemaGuard
schema: ResolvePublishedSchemaGuard, appLanguages: ResolveAppLanguagesGuard
},
data: {
disableHistory: true
@ -55,7 +57,7 @@ const routes: Routes = [
path: ':contentId',
component: ContentPageComponent,
resolve: {
schema: ResolvePublishedSchemaGuard
schema: ResolvePublishedSchemaGuard, appLanguages: ResolveAppLanguagesGuard
},
children: [
{
@ -79,6 +81,7 @@ const routes: Routes = [
RouterModule.forChild(routes)
],
declarations: [
ContentFieldComponent,
ContentPageComponent,
ContentsPageComponent,
SchemasPageComponent

86
src/Squidex/app/features/content/pages/content/content-field.component.html

@ -0,0 +1,86 @@
<div class="table-items-row">
<label>
{{field|displayName:'properties.label':'name'}} <span class="field-required" [class.hidden]="!field.properties.isRequired">*</span>
</label>
<span class="field-disabled" *ngIf="fieldForm.disabled">Disabled</span>
<div [formGroup]="fieldForm">
<div *ngIf="field.properties.isLocalizable">
<div class="btn-group btn-group-sm languages-buttons" role="group">
<button type="button" class="btn btn-secondary" *ngFor="let language of languages" [attr.title]="language.englishName"
[class.btn-danger]="fieldForm.controls[language.iso2Code].invalid && (fieldForm.controls[language.iso2Code].touched || contentFormSubmitted)"
[class.active]="language.iso2Code == selectedLanguage" (click)="selectLanguage(language)">
{{language.iso2Code}}
</button>
</div>
</div>
<div *ngFor="let language of fieldLanguages">
<div *ngIf="language == selectedLanguage">
<sqx-control-errors [for]="language" fieldName="{{field|displayName:'properties.label':'name'}}" [submitted]="contentFormSubmitted"></sqx-control-errors>
<div [ngSwitch]="field.properties.fieldType">
<div *ngSwitchCase="'number'">
<div [ngSwitch]="field.properties.editor">
<div *ngSwitchCase="'Input'">
<input class="form-control" type="number" [formControlName]="language">
</div>
<div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControlName]="language">
<option *ngFor="let value of field.properties.allowedValues">{{value}}</option>
</select>
</div>
<div *ngSwitchCase="'Radio'">
<div class="form-check form-check-inline" *ngFor="let value of field.properties.allowedValues">
<label class="form-check-label">
<input class="form-check-input" type="radio" value="{{value}}" [formControlName]="language"> {{value}}
</label>
</div>
</div>
</div>
</div>
<div *ngSwitchCase="'string'">
<div [ngSwitch]="field.properties.editor">
<div *ngSwitchCase="'Input'">
<input class="form-control" type="text" [formControlName]="language">
</div>
<div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControlName]="language">
<option *ngFor="let value of field.properties.allowedValues">{{value}}</option>
</select>
</div>
<div *ngSwitchCase="'TextArea'">
<textarea class="form-control" [formControlName]="language"></textarea>
</div>
<div *ngSwitchCase="'Radio'">
<div class="form-check form-check-inline" *ngFor="let value of field.properties.allowedValues">
<label class="form-check-label">
<input class="form-check-input" type="radio" value="{{value}}" [formControlName]="language"> {{value}}
</label>
</div>
</div>
</div>
</div>
<div *ngSwitchCase="'boolean'">
<div [ngSwitch]="field.properties.editor">
<div *ngSwitchCase="'Checkbox'">
<div class="form-check form-check-inline">
<label class="form-check-label">
<input class="form-check-input" type="checkbox" [formControlName]="language">
</label>
</div>
</div>
<div *ngSwitchCase="'Toggle'">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="form-hint" *ngIf="field.properties.hints && field.properties.hints.length > 0">
{{field.properties.hints}}
</div>
</div>

22
src/Squidex/app/features/content/pages/content/content-field.component.scss

@ -0,0 +1,22 @@
@import '_vars';
@import '_mixins';
.table-items-row {
position: relative;
}
.languages-buttons {
@include absolute(1rem, 1.25rem, auto, auto);
}
.field {
&-required {
color: $color-theme-error;
}
&-disabled {
color: $color-border-dark;
font-size: .8rem;
font-weight: normal;
}
}

56
src/Squidex/app/features/content/pages/content/content-field.component.ts

@ -0,0 +1,56 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { Component, Input, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
import {
AppLanguageDto,
FieldDto
} from 'shared';
@Component({
selector: 'sqx-content-field',
styleUrls: ['./content-field.component.scss'],
templateUrl: './content-field.component.html'
})
export class ContentFieldComponent implements OnInit {
@Input()
public field: FieldDto;
@Input()
public fieldForm: FormGroup;
@Input()
public languages: AppLanguageDto[];
@Input()
public contentFormSubmitted: boolean;
public fieldLanguages: string[];
public selectedLanguage: string;
public selectLanguage(language: AppLanguageDto) {
this.selectedLanguage = language.iso2Code;
}
public ngOnInit() {
if (this.field.isDisabled) {
this.fieldForm.disable();
}
if (this.field.properties.isLocalizable) {
this.fieldLanguages = this.languages.map(t => t.iso2Code);
this.selectedLanguage = this.fieldLanguages[0];
} else {
this.fieldLanguages = ['iv'];
this.selectedLanguage = 'iv';
}
}
}

86
src/Squidex/app/features/content/pages/content/content-page.component.html

@ -3,85 +3,25 @@
<form [formGroup]="contentForm" (ngSubmit)="saveContent()">
<div class="panel panel-light" >
<div class="panel-header">
<div class="float-xs-right">
<button type="submit" class="btn btn-primary">
Save
</button>
</div>
<h3 class="panel-title" *ngIf="isNewMode">New {{schema|displayName}}</h3>
<div class="panel-header-title-row">
<div class="float-xs-right">
<button type="submit" class="btn btn-primary">
Save
</button>
</div>
<h3 class="panel-title" *ngIf="isNewMode">New {{schema|displayName}}</h3>
<a class="panel-close" routerLink="../">
<i class="icon-close"></i>
</a>
<a class="panel-close" routerLink="../">
<i class="icon-close"></i>
</a>
</div>
</div>
<div class="panel-main">
<div class="panel-content">
<div *ngFor="let field of schema.fields">
<div class="table-items-row">
<label>{{field|displayName:'properties.label':'name'}}</label>
<div [formGroup]="contentForm.controls[field.name]">
<div *ngFor="let language of languages">
<sqx-control-errors [for]="language" fieldName="{{field|displayName:'properties.label':'name'}}" [submitted]="contentFormSubmitted"></sqx-control-errors>
<div [ngSwitch]="field.properties.fieldType">
<div *ngSwitchCase="'number'">
<div [ngSwitch]="field.properties.editor">
<div *ngSwitchCase="'Input'">
<input class="form-control" type="number" [formControlName]="language">
</div>
<div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControlName]="language">
<option *ngFor="let value of field.properties.allowedValues">{{value}}</option>
</select>
</div>
<div *ngSwitchCase="'Radio'">
<label *ngFor="let value of field.properties.allowedValues">
<input type="radio" value="{{value}}" [formControlName]="language"> {{value}}
</label>
</div>
</div>
</div>
<div *ngSwitchCase="'string'">
<div [ngSwitch]="field.properties.editor">
<div *ngSwitchCase="'Input'">
<input class="form-control" type="text" [formControlName]="language">
</div>
<div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControlName]="language">
<option *ngFor="let value of field.properties.allowedValues">{{value}}</option>
</select>
</div>
<div *ngSwitchCase="'TextArea'">
<textarea class="form-control" [formControlName]="language"></textarea>
</div>
<div *ngSwitchCase="'Radio'">
<label *ngFor="let value of field.properties.allowedValues">
<input type="radio" value="{{value}}" [formControlName]="language"> {{value}}
</label>
</div>
</div>
</div>
<div *ngSwitchCase="'boolean'">
<div [ngSwitch]="field.properties.editor">
<div *ngSwitchCase="'Checkbox'">
<div class="form-check">
<input type="checkbox" [formControlName]="language">
</div>
</div>
<div *ngSwitchCase="'Toggle'">
</div>
</div>
</div>
</div>
</div>
</div>
<div class="form-hint" *ngIf="field.properties.hints && field.properties.hints.length > 0">
{{field.properties.hints}}
</div>
</div>
<sqx-content-field [field]="field" [fieldForm]="contentForm.controls[field.name]" [languages]="languages" [contentFormSubmitted]="contentFormSubmitted"></sqx-content-field>
</div>
</div>
<div class="panel-sidebar" *ngIf="!isNewMode">

4
src/Squidex/app/features/content/pages/content/content-page.component.scss

@ -4,8 +4,4 @@
.panel {
min-width: 36rem;
max-width: 36rem;
}
label {
margin: .4rem;
}

49
src/Squidex/app/features/content/pages/content/content-page.component.ts

@ -9,10 +9,14 @@ import { Component } from '@angular/core';
import { AbstractControl, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { ContentAdded } from './../messages';
import {
AppComponentBase,
AppLanguageDto,
AppsStoreService,
ContentsService,
MessageBus,
NotificationService,
NumberFieldPropertiesDto,
SchemaDetailsDto,
@ -32,13 +36,14 @@ export class ContentPageComponent extends AppComponentBase {
public contentFormSubmitted = false;
public contentForm: FormGroup;
public languages = ['iv'];
public languages: AppLanguageDto[] = [];
public isNewMode = false;
constructor(apps: AppsStoreService, notifications: NotificationService, users: UsersProviderService,
private readonly contentsService: ContentsService,
private readonly route: ActivatedRoute
private readonly route: ActivatedRoute,
private readonly messageBus: MessageBus
) {
super(apps, notifications, users);
}
@ -48,6 +53,10 @@ export class ContentPageComponent extends AppComponentBase {
this.isNewMode = !contentId;
});
this.route.data.map(p => p['appLanguages']).subscribe((languages: AppLanguageDto[]) => {
this.languages = languages;
});
this.route.data.map(p => p['schema']).subscribe((schema: SchemaDetailsDto) => {
this.schema = schema;
@ -59,7 +68,7 @@ export class ContentPageComponent extends AppComponentBase {
this.contentFormSubmitted = true;
if (this.contentForm.valid) {
this.contentForm.disable();
this.disable();
const data = this.contentForm.value;
@ -67,17 +76,37 @@ export class ContentPageComponent extends AppComponentBase {
.switchMap(app => this.contentsService.postContent(app, this.schema.name, data))
.subscribe(() => {
this.reset();
this.messageBus.publish(new ContentAdded());
}, error => {
this.contentForm.enable();
this.notifyError(error);
this.enable();
});
}
}
public reset() {
this.enable();
this.contentForm.reset();
this.contentFormSubmitted = false;
}
public enable() {
for (const field of this.schema.fields.filter(f => !f.isDisabled)) {
const fieldForm = this.contentForm.controls[field.name];
fieldForm.enable();
}
}
public disable() {
for (const field of this.schema.fields.filter(f => !f.isDisabled)) {
const fieldForm = this.contentForm.controls[field.name];
fieldForm.disable();
}
}
private setupForm(schema: SchemaDetailsDto) {
const controls: { [key: string]: AbstractControl } = {};
@ -102,9 +131,15 @@ export class ContentPageComponent extends AppComponentBase {
}
}
const group = new FormGroup({
'iv': new FormControl(undefined, validators)
});
const group = new FormGroup({});
if (field.properties.isLocalizable) {
for (let language of this.languages) {
group.addControl(language.iso2Code, new FormControl(undefined, validators));
}
} else {
group.addControl('iv', new FormControl(undefined, validators));
}
controls[field.name] = group;
}

75
src/Squidex/app/features/content/pages/contents/contents-page.component.html

@ -2,21 +2,78 @@
<div class="panel panel-light">
<div class="panel-header">
<div class="float-xs-right">
<a class="btn btn-success" [routerLink]="['new']">
<i class="icon-plus"></i> New
</a>
</div>
<div class="panel-header-title-row">
<div class="float-xs-right">
<div class="btn-group languages-buttons" role="group">
<button type="button" class="btn btn-secondary" *ngFor="let language of languages" [attr.title]="language.englishName" [class.active]="language == selectedLanguage" (click)="selectLanguage(language)">
{{language.iso2Code}}
</button>
</div>
<a class="btn btn-success" [routerLink]="['new']">
<i class="icon-plus"></i> New
</a>
</div>
<h3 class="panel-title">{{schema|displayName}} Contents</h3>
<h3 class="panel-title">{{schema|displayName}} Contents</h3>
<a class="panel-close" routerLink="../">
<i class="icon-close"></i>
</a>
<a class="panel-close" routerLink="../">
<i class="icon-close"></i>
</a>
</div>
</div>
<div class="panel-main">
<div class="panel-content">
<table class="table table-items table-fixed" *ngIf="contents">
<colgroup>
<col *ngFor="let field of contentFields" [style.width]="columnWidth + '%'" />
<col style="width: 170px" />
<col style="width: 80px" />
<col style="width: 80px" />
</colgroup>
<thead>
<tr>
<th *ngFor="let field of contentFields">
<span class="field">{{field|displayName:'properties.label':'name'}}</span>
</th>
<th>
Updated
</th>
<th>
By
</th>
<th>
Options
</th>
</tr>
</thead>
<tbody>
<template ngFor let-content [ngForOf]="contents.items">
<tr>
<td *ngFor="let field of contentFields">
<span class="field">
{{getFieldContent(content, field)}}
</span>
</td>
<td>
{{content.lastModified|fromNow}}
</td>
<td>
<img class="user-picture" [attr.title]="userName(content.lastModifiedBy) | async" [attr.src]="userPicture(content.lastModifiedBy, true) | async" />
</td>
<td>
<button type="button" class="btn btn-simple">
<i class="icon-dots"></i>
</button>
</td>
</tr>
<tr class="spacer"></tr>
</template>
</tbody>
</table>
</div>
<div class="panel-sidebar">
<div class="nav nav-pills nav-stacked nav-light">

22
src/Squidex/app/features/content/pages/contents/contents-page.component.scss

@ -2,6 +2,24 @@
@import '_mixins';
.panel {
min-width: 36rem;
max-width: 36rem;
min-width: 64rem;
max-width: 64rem;
}
.field {
@include truncate;
}
.languages-buttons {
margin-right: 1rem;
}
.user-picture {
& {
@include circle(2.2rem);
}
&:not([src]) {
@include opacity(0);
}
}

83
src/Squidex/app/features/content/pages/contents/contents-page.component.ts

@ -7,10 +7,19 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { ContentAdded } from './../messages';
import {
AppComponentBase,
AppLanguageDto,
AppsStoreService,
ContentDto,
ContentsDto,
ContentsService,
FieldDto,
MessageBus,
NotificationService,
SchemaDetailsDto,
UsersProviderService
@ -22,18 +31,90 @@ import {
templateUrl: './contents-page.component.html'
})
export class ContentsPageComponent extends AppComponentBase implements OnInit {
private messageSubscription: Subscription;
public schema: SchemaDetailsDto;
public contents: ContentsDto;
public contentFields: FieldDto[];
public languages: AppLanguageDto[] = [];
public selectedLanguage: AppLanguageDto;
public page = 0;
public query = '';
public get columnWidth() {
return 100 / this.contentFields.length;
}
constructor(apps: AppsStoreService, notifications: NotificationService, users: UsersProviderService,
private readonly route: ActivatedRoute
private readonly contentsService: ContentsService,
private readonly route: ActivatedRoute,
private readonly messageBus: MessageBus
) {
super(apps, notifications, users);
}
public selectLanguage(language: AppLanguageDto) {
this.selectedLanguage = language;
}
public ngOnInit() {
this.messageSubscription =
this.messageBus.of(ContentAdded).delay(2000).subscribe(message => {
this.load();
});
this.route.data.map(p => p['appLanguages']).subscribe((languages: AppLanguageDto[]) => {
this.languages = languages;
this.selectedLanguage = languages.filter(t => t.isMasterLanguage)[0];
});
this.route.data.map(p => p['schema']).subscribe(schema => {
this.schema = schema;
this.reset();
this.loadFields();
this.load();
});
}
public getFieldContent(content: ContentDto, field: FieldDto): any {
const contentField = content.data[field.name];
if (!contentField) {
return '';
}
if (field.properties.isLocalizable) {
return contentField[this.selectedLanguage.iso2Code];
} else {
return contentField['iv'];
}
}
private reset() {
this.page = 0;
}
private loadFields() {
this.contentFields = this.schema.fields.filter(x => x.properties.isListField);
if (this.contentFields.length === 0 && this.schema.fields.length > 0) {
this.contentFields = [this.schema.fields[0]];
}
}
private load() {
this.appName()
.switchMap(app => this.contentsService.getContents(app, this.schema.name, 20, this.page * 20, this.query))
.subscribe(dtos => {
this.contents = dtos;
}, error => {
this.notifyError(error);
});
}
}

8
src/Squidex/app/features/content/pages/messages.ts

@ -0,0 +1,8 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
export class ContentAdded { }

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

@ -2,7 +2,7 @@
<div class="panel panel-dark">
<div class="panel-header">
<div>
<div class="panel-header-title-row">
<h3 class="panel-title">Schemas</h3>
<a class="panel-close" dashboardLink>
@ -10,7 +10,7 @@
</a>
</div>
<div class="subheader">
<div class="panel-header-row">
<div class="search-form">
<input class="form-control form-control-dark" [formControl]="schemasFilter" placeholder="Search for schemas..." />

13
src/Squidex/app/features/content/pages/schemas/schemas-page.component.scss

@ -6,18 +6,6 @@
max-width: 20rem;
}
.panel-header {
min-height: 8rem;
max-height: 8rem;
}
.subheader {
@include flex-box;
@include flex-flow(row);
margin-top: 2rem;
margin-right: -2.5rem;
}
.search-form {
& {
@include flex-grow(1);
@ -45,6 +33,7 @@
& {
padding-left: $panel-padding;
padding-right: $panel-padding;
cursor: pointer;
}
&:hover,

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

@ -109,6 +109,22 @@
</span>
</div>
</div>
<div class="form-group row">
<div class="form-check offset-xs-3 col-xs-6">
<label class="form-check-label">
<input class="form-check-input" type="checkbox" formControlName="isListField"> List Field
</label>
</div>
</div>
<div class="form-group row">
<div class="form-check offset-xs-3 col-xs-6">
<label class="form-check-label">
<input class="form-check-input" type="checkbox" formControlName="isLocalizable"> Localizable
</label>
</div>
</div>
</div>
<div class="field-details-tab" [class.hidden]="selectedTab !== 1">

4
src/Squidex/app/features/schemas/pages/schema/field.component.scss

@ -84,9 +84,9 @@ $field-header: #e7ebef;
&::before {
@include caret-top;
@include absolute(-.625rem, 6rem, auto, auto);
@include absolute(-.5rem, 5.5rem, auto, auto);
border-color: transparent transparent $color-border;
border-width: .5rem;
border-width: .6rem;
}
&-tab {

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

@ -65,7 +65,9 @@ export class FieldComponent implements OnInit {
[
Validators.maxLength(100)
]],
isRequired: [false]
isRequired: [false],
isListField: [false],
isLocalizable: [false]
});
constructor(

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

@ -2,13 +2,13 @@
<div class="panel panel-light">
<div class="panel-header">
<div>
<div class="panel-header-title-row">
<div class="float-xs-right">
<div class="btn-group btn-group-sm" data-toggle="buttons">
<button type="button" class="btn btn-publishing btn-default" [class.btn-success]="isPublished" [disabled]="isPublished" (click)="publish()">
<button type="button" class="btn btn-publishing btn-secondary" [class.btn-success]="isPublished" [disabled]="isPublished" (click)="publish()">
Published
</button>
<button type="button" class="btn btn-publishing btn-default" [class.btn-danger]="!isPublished" [disabled]="!isPublished" (click)="unpublish()">
<button type="button" class="btn btn-publishing btn-secondary" [class.btn-danger]="!isPublished" [disabled]="!isPublished" (click)="unpublish()">
Unpublished
</button>
</div>

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

@ -2,14 +2,15 @@
<div class="panel panel-dark">
<div class="panel-header">
<div>
<div class="panel-header-title-row">
<h3 class="panel-title">Schemas</h3>
<a class="panel-close" dashboardLink>
<i class="icon-close"></i>
</a>
</div>
<div class="subheader">
<div class="panel-header-row">
<button class="btn btn-success subheader-button" (click)="addSchemaDialog.show()">
<i class="icon-plus"></i>
</button>

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

@ -8,28 +8,14 @@ $button-size: 2.5rem;
max-width: 30rem;
}
.panel-header {
min-height: 7rem;
max-height: 6.7rem;
}
.subheader {
& {
@include flex-box;
@include flex-flow(row);
margin-top: 2rem;
margin-right: -2.5rem;
}
&-button {
height: $button-size;
margin-right: .4rem;
min-width: $button-size;
max-width: $button-size;
padding-left: 0;
padding-right: 0;
font-size: 1.1rem;
}
.subheader-button {
height: $button-size;
margin-right: .4rem;
min-width: $button-size;
max-width: $button-size;
padding-left: 0;
padding-right: 0;
font-size: 1.1rem;
}
.search-form {
@ -61,6 +47,7 @@ $button-size: 2.5rem;
& {
padding-left: $panel-padding;
padding-right: $panel-padding;
cursor: pointer;
}
&:hover,

2
src/Squidex/app/features/settings/pages/clients/client.component.html

@ -9,7 +9,7 @@
<tr>
<td colspan="2">
<div class="float-xs-right">
<button class="btn btn-default" (click)="createToken(client)">Create Token</button>
<button class="btn btn-secondary" (click)="createToken(client)">Create Token</button>
</div>
<div class="client-header">

2
src/Squidex/app/features/settings/pages/clients/client.component.scss

@ -14,6 +14,8 @@ $color-editor: #eceeef;
&-expires {
font-size: .8rem;
font-weight: normal;
margin-bottom: .75rem;
}
&-edit {

10
src/Squidex/app/features/settings/pages/clients/clients-page.component.html

@ -2,11 +2,13 @@
<div class="panel panel-light">
<div class="panel-header">
<h3 class="panel-title">Clients</h3>
<div class="panel-header-title-row">
<h3 class="panel-title">Clients</h3>
<a class="panel-close" routerLink="../">
<i class="icon-close"></i>
</a>
<a class="panel-close" routerLink="../">
<i class="icon-close"></i>
</a>
</div>
</div>
<div class="panel-main">

5
src/Squidex/app/features/settings/pages/clients/clients-page.component.scss

@ -24,9 +24,4 @@
font-weight: lighter;
padding: .2rem 1rem;
}
}
.access-token {
resize: none;
height: 18rem;
}

12
src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html

@ -2,11 +2,13 @@
<div class="panel panel-light">
<div class="panel-header">
<h3 class="panel-title">Contributors</h3>
<div class="panel-header-title-row">
<h3 class="panel-title">Contributors</h3>
<a class="panel-close" routerLink="../">
<i class="icon-close"></i>
</a>
<a class="panel-close" routerLink="../">
<i class="icon-close"></i>
</a>
</div>
</div>
<div class="panel-main">
@ -41,7 +43,7 @@
<template ngFor let-contributor [ngForOf]="appContributors">
<tr>
<td>
<img class="user-picture" [attr.src]="userPicture(contributor.contributorId) | async" />
<img class="user-picture" [attr.title]="userName(contributor.contributorId) | async" [attr.src]="userPicture(contributor.contributorId) | async" />
</td>
<td>
<span class="user-name">{{userName(contributor.contributorId) | async}}</span>

10
src/Squidex/app/features/settings/pages/languages/languages-page.component.html

@ -2,11 +2,13 @@
<div class="panel panel-light">
<div class="panel-header">
<h3 class="panel-title">Settings</h3>
<div class="panel-header-title-row">
<h3 class="panel-title">Settings</h3>
<a class="panel-close" routerLink="../">
<i class="icon-close"></i>
</a>
<a class="panel-close" routerLink="../">
<i class="icon-close"></i>
</a>
</div>
</div>
<div class="panel-main">

10
src/Squidex/app/features/settings/settings-area.component.html

@ -1,10 +1,12 @@
<div class="panel panel-dark">
<div class="panel-header">
<h3 class="panel-title">Settings</h3>
<div class="panel-header-title-row">
<h3 class="panel-title">Settings</h3>
<a class="panel-close" dashboardLink>
<i class="icon-close"></i>
</a>
<a class="panel-close" dashboardLink>
<i class="icon-close"></i>
</a>
</div>
</div>
<div class="panel-main">

8
src/Squidex/app/framework/angular/control-errors.component.ts

@ -2,10 +2,10 @@
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
* Copyright (c) Sebastian Stehle. All rights r vbeserved
*/
import { Component, Host, Input, OnChanges, OnInit, Optional } from '@angular/core';
import { Component, Host, Input, OnChanges, Optional } from '@angular/core';
import { AbstractControl, FormGroupDirective } from '@angular/forms';
import { fadeAnimation } from './animations';
@ -30,7 +30,7 @@ const DEFAULT_ERRORS: { [key: string]: string } = {
fadeAnimation
]
})
export class ControlErrorsComponent implements OnChanges, OnInit {
export class ControlErrorsComponent implements OnChanges {
private displayFieldName: string;
private control: AbstractControl;
@ -96,9 +96,7 @@ export class ControlErrorsComponent implements OnChanges, OnInit {
} else if (this.for) {
this.displayFieldName = this.for.substr(0, 1).toUpperCase() + this.for.substr(1);
}
}
public ngOnInit() {
this.control = this.formGroupDirective.form.controls[this.for];
}
}

28
src/Squidex/app/shared/app-component-base.ts

@ -27,12 +27,32 @@ export abstract class AppComponentBase {
return this.appsStore.selectedApp.map(a => a!.name);
}
public userEmail(userId: string): Observable<string> {
return this.usersProvider.getUser(userId).map(u => u.email);
public userEmail(userId: string, isRef: boolean = false): Observable<string> {
if (isRef) {
const parts = userId.split(':');
if (parts[0] === 'subject') {
return this.usersProvider.getUser(parts[1]).map(u => u.email);
} else {
return null;
}
} else {
return this.usersProvider.getUser(userId).map(u => u.email);
}
}
public userPicture(userId: string): Observable<string> {
return this.usersProvider.getUser(userId).map(u => u.pictureUrl);
public userPicture(userId: string, isRef: boolean = false): Observable<string> {
if (isRef) {
const parts = userId.split(':');
if (parts[0] === 'subject') {
return this.usersProvider.getUser(parts[1]).map(u => u.pictureUrl);
} else {
return null;
}
} else {
return this.usersProvider.getUser(userId).map(u => u.pictureUrl);
}
}
public userName(userId: string, isRef: boolean = false, placeholder = 'Me'): Observable<string> {

12
src/Squidex/app/shared/components/history.component.html

@ -2,18 +2,20 @@
<div class="panel panel-light">
<div class="panel-header">
<h3 class="panel-title">Activity</h3>
<div class="panel-header-title-row">
<h3 class="panel-title">Activity</h3>
<a class="panel-close" routerLink="../">
<i class="icon-close"></i>
</a>
<a class="panel-close" routerLink="../">
<i class="icon-close"></i>
</a>
</div>
</div>
<div class="panel-main">
<div class="panel-content panel-content-blank">
<div *ngFor="let event of events | async" class="event">
<div class="event-left">
<img class="event-picture" [attr.src]="actorProfile(event.actor) | async" />
<img class="event-picture"[attr.title]="actorName(event.actor) | async" [attr.src]="actorProfile(event.actor) | async" />
</div>
<div class="event-main">
<div class="event-message">

1
src/Squidex/app/shared/declarations.ts

@ -12,6 +12,7 @@ export * from './components/history.component';
export * from './guards/app-must-exist.guard';
export * from './guards/must-be-authenticated.guard';
export * from './guards/must-be-not-authenticated.guard';
export * from './guards/resolve-app-languages.guard';
export * from './guards/resolve-published-schema.guard';
export * from './guards/resolve-schema.guard';

83
src/Squidex/app/shared/guards/resolve-app-languages.guard.spec.ts

@ -0,0 +1,83 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { IMock, Mock } from 'typemoq';
import { Observable } from 'rxjs';
import { AppLanguageDto, AppLanguagesService } from 'shared';
import { ResolveAppLanguagesGuard } from './resolve-app-languages.guard';
import { RouterMockup } from './router-mockup';
describe('ResolveAppLanguagesGuard', () => {
const route = {
params: {
appName: 'my-app'
}
};
let appLanguagesService: IMock<AppLanguagesService>;
beforeEach(() => {
appLanguagesService = Mock.ofType(AppLanguagesService);
});
it('should throw if route does not contain parameter', () => {
const guard = new ResolveAppLanguagesGuard(appLanguagesService.object, <any>new RouterMockup());
expect(() => guard.resolve(<any>{ params: {} }, <any>{})).toThrow('Route must contain app.');
});
it('should navigate to 404 page if languages are not found', (done) => {
appLanguagesService.setup(x => x.getLanguages('my-app'))
.returns(() => Observable.of(null!));
const router = new RouterMockup();
const guard = new ResolveAppLanguagesGuard(appLanguagesService.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
expect(result).toBeFalsy();
expect(router.lastNavigation).toEqual(['/404']);
done();
});
});
it('should navigate to 404 page if languages loading fails', (done) => {
appLanguagesService.setup(x => x.getLanguages('my-app'))
.returns(() => Observable.throw(null!));
const router = new RouterMockup();
const guard = new ResolveAppLanguagesGuard(appLanguagesService.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
expect(result).toBeFalsy();
expect(router.lastNavigation).toEqual(['/404']);
done();
});
});
it('should return schema if loading succeeded', (done) => {
const languages: AppLanguageDto[] = [];
appLanguagesService.setup(x => x.getLanguages('my-app'))
.returns(() => Observable.of(languages));
const router = new RouterMockup();
const guard = new ResolveAppLanguagesGuard(appLanguagesService.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
expect(result).toBe(languages);
done();
});
});
});

62
src/Squidex/app/shared/guards/resolve-app-languages.guard.ts

@ -0,0 +1,62 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
import { AppLanguageDto, AppLanguagesService } from './../services/app-languages.service';
@Injectable()
export class ResolveAppLanguagesGuard implements Resolve<AppLanguageDto[]> {
constructor(
private readonly appLanguagesService: AppLanguagesService,
private readonly router: Router
) {
}
public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<AppLanguageDto[]> {
const appName = this.findParameter(route, 'appName');
if (!appName) {
throw 'Route must contain app and schema name.';
}
const result =
this.appLanguagesService.getLanguages(appName).toPromise()
.then(dto => {
if (!dto) {
this.router.navigate(['/404']);
return null;
}
return dto;
}).catch(() => {
this.router.navigate(['/404']);
return null;
});
return result;
}
private findParameter(route: ActivatedRouteSnapshot, name: string): string | null {
let result: string | null = null;
while (route) {
result = route.params[name];
if (result) {
break;
}
route = route.parent;
}
return result;
}
}

2
src/Squidex/app/shared/module.ts

@ -25,6 +25,7 @@ import {
LanguageService,
MustBeAuthenticatedGuard,
MustBeNotAuthenticatedGuard,
ResolveAppLanguagesGuard,
ResolvePublishedSchemaGuard,
ResolveSchemaGuard,
SchemasService,
@ -64,6 +65,7 @@ export class SqxSharedModule {
LanguageService,
MustBeAuthenticatedGuard,
MustBeNotAuthenticatedGuard,
ResolveAppLanguagesGuard,
ResolvePublishedSchemaGuard,
ResolveSchemaGuard,
SchemasService,

49
src/Squidex/app/shared/services/contents.service.spec.ts

@ -14,6 +14,7 @@ import {
AuthService,
EntityCreatedDto,
ContentDto,
ContentsDto,
ContentsService,
DateTime
} from './../';
@ -32,38 +33,42 @@ describe('ContentsService', () => {
.returns(() => Observable.of(
new Response(
new ResponseOptions({
body: [{
id: 'id1',
isPublished: true,
created: '2016-12-12T10:10',
createdBy: 'Created1',
lastModified: '2017-12-12T10:10',
lastModifiedBy: 'LastModifiedBy1',
data: {}
}, {
id: 'id2',
isPublished: true,
created: '2016-10-12T10:10',
createdBy: 'Created2',
lastModified: '2017-10-12T10:10',
lastModifiedBy: 'LastModifiedBy2',
data: {}
}]
body: {
total: 10,
items: [{
id: 'id1',
isPublished: true,
created: '2016-12-12T10:10',
createdBy: 'Created1',
lastModified: '2017-12-12T10:10',
lastModifiedBy: 'LastModifiedBy1',
data: {}
}, {
id: 'id2',
isPublished: true,
created: '2016-10-12T10:10',
createdBy: 'Created2',
lastModified: '2017-10-12T10:10',
lastModifiedBy: 'LastModifiedBy2',
data: {}
}]
}
})
)
))
.verifiable(Times.once());
let contents: ContentDto[] | null = null;
let contents: ContentsDto | null = null;
contentsService.getContents('my-app', 'my-schema', 17, 13, 'my-query').subscribe(result => {
contents = result;
}).unsubscribe();
expect(contents).toEqual([
new ContentDto('id1', true, 'Created1', 'LastModifiedBy1', DateTime.parseISO_UTC('2016-12-12T10:10'), DateTime.parseISO_UTC('2017-12-12T10:10'), {}),
new ContentDto('id2', true, 'Created2', 'LastModifiedBy2', DateTime.parseISO_UTC('2016-10-12T10:10'), DateTime.parseISO_UTC('2017-10-12T10:10'), {})
]);
expect(contents).toEqual(
new ContentsDto(10, [
new ContentDto('id1', true, 'Created1', 'LastModifiedBy1', DateTime.parseISO_UTC('2016-12-12T10:10'), DateTime.parseISO_UTC('2017-12-12T10:10'), {}),
new ContentDto('id2', true, 'Created2', 'LastModifiedBy2', DateTime.parseISO_UTC('2016-10-12T10:10'), DateTime.parseISO_UTC('2017-10-12T10:10'), {})
]));
authService.verifyAll();
});

16
src/Squidex/app/shared/services/contents.service.ts

@ -18,6 +18,14 @@ import {
import { AuthService } from './auth.service';
export class ContentsDto {
constructor(
public readonly total: number,
public readonly items: ContentDto[]
) {
}
}
export class ContentDto {
constructor(
public readonly id: string,
@ -39,15 +47,15 @@ export class ContentsService {
) {
}
public getContents(appName: string, schemaName: string, take: number, skip: number, query: string): Observable<ContentDto[]> {
public getContents(appName: string, schemaName: string, take: number, skip: number, query: string): Observable<ContentsDto> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}?take=${take}&skip=${skip}&query=${query}&nonPublished=true`);
return this.authService.authGet(url)
.map(response => response.json())
.map(response => {
const items: any[] = response;
const items: any[] = response.items;
return items.map(item => {
return new ContentsDto(response.total, items.map(item => {
return new ContentDto(
item.id,
item.isPublished,
@ -56,7 +64,7 @@ export class ContentsService {
DateTime.parseISO_UTC(item.created),
DateTime.parseISO_UTC(item.lastModified),
item.data);
});
}));
})
.catchError('Failed to load contents. Please reload.');
}

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

@ -25,19 +25,19 @@ export function createProperties(fieldType: string, values: Object | null = null
case 'number':
properties =
new NumberFieldPropertiesDto(
undefined, undefined, undefined, false, 'Input',
undefined, undefined, undefined, false, false, false, 'Input',
undefined, undefined, undefined, undefined);
break;
case 'string':
properties =
new StringFieldPropertiesDto(
undefined, undefined, undefined, false, 'Input',
undefined, undefined, undefined, false, false, false, 'Input',
undefined, undefined, undefined, undefined, undefined, undefined);
break;
case 'boolean':
properties =
new BooleanFieldPropertiesDto(
undefined, undefined, undefined, false, 'Checkbox',
undefined, undefined, undefined, false, false, false, 'Checkbox',
undefined);
break;
default:
@ -97,27 +97,35 @@ export abstract class FieldPropertiesDto {
public readonly label?: string,
public readonly hints?: string,
public readonly placeholder?: string,
public readonly isRequired: boolean = false
public readonly isRequired: boolean = false,
public readonly isListField: boolean = false,
public readonly isLocalizable: boolean = false
) {
}
}
export class NumberFieldPropertiesDto extends FieldPropertiesDto {
constructor(label: string | undefined, hints: string | undefined, placeholder: string | undefined, isRequired: boolean,
constructor(label: string | undefined, hints: string | undefined, placeholder: string | undefined,
isRequired: boolean,
isListField: boolean,
isLocalizable: boolean,
public readonly editor: string,
public readonly defaultValue?: number,
public readonly maxValue?: number,
public readonly minValue?: number,
public readonly allowedValues?: number[]
) {
super(label, hints, placeholder, isRequired);
super(label, hints, placeholder, isRequired, isListField, isLocalizable);
this['fieldType'] = 'number';
}
}
export class StringFieldPropertiesDto extends FieldPropertiesDto {
constructor(label: string | undefined, hints: string | undefined, placeholder: string | undefined, isRequired: boolean,
constructor(label: string | undefined, hints: string | undefined, placeholder: string | undefined,
isRequired: boolean,
isListField: boolean,
isLocalizable: boolean,
public readonly editor: string,
public readonly defaultValue?: string,
public readonly pattern?: string,
@ -126,18 +134,21 @@ export class StringFieldPropertiesDto extends FieldPropertiesDto {
public readonly maxLength?: number | null,
public readonly allowedValues?: string[]
) {
super(label, hints, placeholder, isRequired);
super(label, hints, placeholder, isRequired, isListField, isLocalizable);
this['fieldType'] = 'string';
}
}
export class BooleanFieldPropertiesDto extends FieldPropertiesDto {
constructor(label: string | undefined, hints: string | undefined, placeholder: string | undefined, isRequired: boolean,
constructor(label: string | undefined, hints: string | undefined, placeholder: string | undefined,
isRequired: boolean,
isListField: boolean,
isLocalizable: boolean,
public readonly editor: string,
public readonly defaultValue?: boolean
) {
super(label, hints, placeholder, isRequired);
super(label, hints, placeholder, isRequired, isListField, isLocalizable);
this['fieldType'] = 'boolean';
}

19
src/Squidex/app/theme/_panels.scss

@ -35,10 +35,19 @@
}
&-header {
min-height: $panel-header;
max-height: $panel-header;
max-height: $panel-header * 3;
}
&-header-row {
@include flex-box;
@include flex-flow(row);
position: relative;
margin-top: 1.2rem;
}
&-header-title-row {
position: relative;
padding-right: 4rem;
padding-right: $panel-sidebar - $panel-padding;
}
&-content {
@ -70,8 +79,8 @@
&-close {
& {
@include absolute($panel-padding, $panel-padding - .125rem, auto, auto);
font-size: 1.5rem;
@include absolute(0, -.125rem, auto, auto);
font-size: 1.3rem;
font-weight: 400;
cursor: pointer;
}

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

@ -39,6 +39,6 @@ $color-card-footer: #fff;
$size-navbar-height: 3.25rem;
$size-sidebar-width: 6.25rem;
$panel-padding: 1.25rem;
$panel-padding: 1.5rem;
$panel-header: 4.6rem;
$panel-sidebar: 3.75rem;

4
src/Squidex/app/theme/_vendor-overrides.scss

@ -3,10 +3,14 @@
$brand-primary: $color-theme-blue;
$brand-success: $color-theme-green;
$brand-danger: $color-theme-error;
$btn-secondary-bg: #dce5e8;
$btn-secondary-border: #dce5e8;
$input-bg-disabled: #eef1f4;
$input-border-color: #dbe4eb;
$card-border-color: $color-border;
$card-cap-bg: $color-card-footer;

31
tests/Squidex.Core.Tests/Contents/ContentDataTests.cs

@ -22,7 +22,10 @@ namespace Squidex.Core.Contents
.AddOrUpdateField(
new NumberField(1, "field1", new NumberFieldProperties { IsLocalizable = true }))
.AddOrUpdateField(
new NumberField(2, "field2", new NumberFieldProperties { IsLocalizable = false }));
new NumberField(2, "field2", new NumberFieldProperties { IsLocalizable = false }))
.AddOrUpdateField(
new NumberField(3, "field3", new NumberFieldProperties { IsLocalizable = false }))
.HideField(3);
private readonly Language[] languages = { Language.GetLanguage("de"), Language.GetLanguage("en") };
private readonly Language masterLanguage = Language.GetLanguage("en");
@ -162,5 +165,31 @@ namespace Squidex.Core.Contents
output.ShouldBeEquivalentTo(expected);
}
[Fact]
public void Should_not_include_hidden_field()
{
var expected =
new Dictionary<string, Dictionary<string, JToken>>
{
["field2"] = new Dictionary<string, JToken>
{
["iv"] = 2
}
};
var input =
ContentData.Empty
.AddField("field2",
ContentFieldData.Empty
.AddValue("iv", 2))
.AddField("field3",
ContentFieldData.Empty
.AddValue("iv", 2));
var output = input.ToApiResponse(schema, languages, masterLanguage);
output.ShouldBeEquivalentTo(expected);
}
}
}

2
tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs

@ -15,7 +15,7 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Read.Apps;
using Squidex.Read.Apps.Services;
using Squidex.Read.Schemas.Repositories;
using Squidex.Read.Schemas;
using Squidex.Read.Schemas.Services;
using Squidex.Write.Contents.Commands;
using Squidex.Write.Utils;

2
tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs

@ -13,7 +13,7 @@ using Moq;
using Squidex.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Read.Schemas.Repositories;
using Squidex.Read.Schemas;
using Squidex.Read.Schemas.Services;
using Squidex.Write.Schemas.Commands;
using Squidex.Write.Utils;

Loading…
Cancel
Save