Browse Source

Merge pull request #253 from Squidex/feature-inline-editing

Feature inline editing
pull/254/head
Sebastian Stehle 9 years ago
committed by GitHub
parent
commit
54b26acba4
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 26
      Dockerfile
  2. 25
      libs/Dockerfile
  3. 0
      libs/keep
  4. 2
      src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs
  5. 2
      src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs
  6. 2
      src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs
  7. 2
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs
  8. 8
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/ElasticSearchActionHandler.cs
  9. 14
      src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs
  10. 5
      src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs
  11. 5
      src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs
  12. 5
      src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs
  13. 4
      src/Squidex/app/app.routes.ts
  14. 188
      src/Squidex/app/features/content/pages/content/content-field.component.html
  15. 51
      src/Squidex/app/features/content/pages/content/content-field.component.ts
  16. 2
      src/Squidex/app/features/content/pages/content/content-history.component.scss
  17. 74
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  18. 9
      src/Squidex/app/features/content/pages/contents/contents-page.component.html
  19. 23
      src/Squidex/app/features/content/pages/contents/contents-page.component.ts
  20. 79
      src/Squidex/app/features/content/shared/content-item.component.html
  21. 4
      src/Squidex/app/features/content/shared/content-item.component.scss
  22. 106
      src/Squidex/app/features/content/shared/content-item.component.ts
  23. 2
      src/Squidex/app/features/content/shared/references-editor.component.html
  24. 2
      src/Squidex/app/features/content/shared/references-editor.component.scss
  25. 3
      src/Squidex/app/features/content/shared/references-editor.component.ts
  26. 2
      src/Squidex/app/features/dashboard/pages/dashboard-page.component.scss
  27. 2
      src/Squidex/app/features/rules/pages/events/rule-events-page.component.scss
  28. 2
      src/Squidex/app/features/schemas/pages/schema/field.component.html
  29. 8
      src/Squidex/app/features/schemas/pages/schema/field.component.scss
  30. 19
      src/Squidex/app/features/schemas/pages/schema/field.component.ts
  31. 2
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.html
  32. 2
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.scss
  33. 7
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts
  34. 10
      src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.html
  35. 8
      src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.ts
  36. 13
      src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.html
  37. 5
      src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.ts
  38. 5
      src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.ts
  39. 8
      src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.ts
  40. 2
      src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.ts
  41. 10
      src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.html
  42. 34
      src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts
  43. 6
      src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.ts
  44. 10
      src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.html
  45. 39
      src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.ts
  46. 6
      src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts
  47. 2
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.scss
  48. 2
      src/Squidex/app/features/settings/pages/clients/client.component.scss
  49. 8
      src/Squidex/app/features/settings/pages/languages/languages-page.component.scss
  50. 2
      src/Squidex/app/framework/angular/control-errors.component.html
  51. 107
      src/Squidex/app/framework/angular/control-errors.component.ts
  52. 6
      src/Squidex/app/framework/angular/http-extensions-impl.ts
  53. 62
      src/Squidex/app/framework/angular/modal-view.directive.ts
  54. 1
      src/Squidex/app/framework/angular/stars.component.scss
  55. 2
      src/Squidex/app/framework/angular/tag-editor.component.scss
  56. 4
      src/Squidex/app/framework/angular/toggle.component.scss
  57. 2
      src/Squidex/app/framework/utils/string-helper.ts
  58. 4
      src/Squidex/app/shared/components/asset.component.scss
  59. 2
      src/Squidex/app/shared/components/history.component.scss
  60. 4
      src/Squidex/app/shared/components/language-selector.component.html
  61. 15
      src/Squidex/app/shared/services/contents.service.spec.ts
  62. 15
      src/Squidex/app/shared/services/contents.service.ts
  63. 6
      src/Squidex/app/shared/services/schemas.fields.spec.ts
  64. 22
      src/Squidex/app/shared/services/schemas.service.ts
  65. 4
      src/Squidex/app/theme/_bootstrap.scss
  66. 4
      src/Squidex/app/theme/_forms.scss
  67. 4
      src/Squidex/app/theme/_lists.scss
  68. 2
      src/Squidex/app/theme/_static.scss
  69. 28
      tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs
  70. 31
      tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs

26
Dockerfile

@ -1,31 +1,7 @@
#
# Stage 1, Prebuild
#
FROM microsoft/aspnetcore-build:2.0.3-jessie as builder
# Install runtime dependencies
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates bzip2 libfontconfig \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Install official PhantomJS release
RUN set -x \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
&& mkdir /srv/var \
&& mkdir /tmp/phantomjs \
# Download Phantom JS
&& curl -L https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 | tar -xj --strip-components=1 -C /tmp/phantomjs \
# Copy binaries only
&& mv /tmp/phantomjs/bin/phantomjs /usr/local/bin \
# Create symbol link
# Clean up
&& apt-get autoremove -y \
&& apt-get clean all \
&& rm -rf /tmp/* /var/lib/apt/lists/*
RUN phantomjs --version
FROM squidex/aspnetcore-build-phantomjs:2.0.3-jessie as builder
COPY src/Squidex/package.json /tmp/package.json

25
libs/Dockerfile

@ -0,0 +1,25 @@
FROM microsoft/aspnetcore-build:2.0.3-jessie
# Install runtime dependencies
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates bzip2 libfontconfig \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Install official PhantomJS release
RUN set -x \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
&& mkdir /srv/var \
&& mkdir /tmp/phantomjs \
# Download Phantom JS
&& curl -L https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 | tar -xj --strip-components=1 -C /tmp/phantomjs \
# Copy binaries only
&& mv /tmp/phantomjs/bin/phantomjs /usr/local/bin \
# Create symbol link
# Clean up
&& apt-get autoremove -y \
&& apt-get clean all \
&& rm -rf /tmp/* /var/lib/apt/lists/*
RUN phantomjs --version

0
libs/keep

2
src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs

@ -14,6 +14,8 @@ namespace Squidex.Domain.Apps.Core.Schemas
{
public bool? DefaultValue { get; set; }
public bool InlineEditable { get; set; }
public BooleanFieldEditor Editor { get; set; }
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)

2
src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs

@ -21,6 +21,8 @@ namespace Squidex.Domain.Apps.Core.Schemas
public double? DefaultValue { get; set; }
public bool InlineEditable { get; set; }
public NumberFieldEditor Editor { get; set; }
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)

2
src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs

@ -19,6 +19,8 @@ namespace Squidex.Domain.Apps.Core.Schemas
public int? MaxLength { get; set; }
public bool InlineEditable { get; set; }
public string DefaultValue { get; set; }
public string Pattern { get; set; }

2
src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs

@ -97,7 +97,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
case ContentDeleted deleted:
{
ruleDescription = $"Delete entry from Index: {action.IndexName}";
ruleDescription = $"Delete entry from Algolia index: {action.IndexName}";
ruleData["Content"] = new JObject();
ruleData["Operation"] = "Delete";

8
src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/ElasticSearchActionHandler.cs

@ -66,7 +66,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{
case ContentCreated created:
{
ruleDescription = $"Add entry to Algolia index: {action.IndexName}";
ruleDescription = $"Add entry to ES index: {action.IndexName}";
ruleData["Operation"] = "Create";
ruleData["Content"] = new JObject(
@ -82,7 +82,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
case ContentUpdated updated:
{
ruleDescription = $"Update entry in Algolia index: {action.IndexName}";
ruleDescription = $"Update entry in ES index: {action.IndexName}";
ruleData["Operation"] = "Update";
ruleData["Content"] = new JObject(
@ -94,7 +94,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
case ContentStatusChanged statusChanged:
{
ruleDescription = $"Update entry in Algolia index: {action.IndexName}";
ruleDescription = $"Update entry in ES index: {action.IndexName}";
ruleData["Operation"] = "Update";
ruleData["Content"] = new JObject(
@ -106,7 +106,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
case ContentDeleted deleted:
{
ruleDescription = $"Delete entry from Index: {action.IndexName}";
ruleDescription = $"Delete entry from ES index: {action.IndexName}";
ruleData["Operation"] = "Delete";
ruleData["Content"] = new JObject();

14
src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs

@ -170,6 +170,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
nameof(properties.MinValue),
nameof(properties.MaxValue));
}
if (properties.InlineEditable && properties.Editor != NumberFieldEditor.Input && properties.Editor != NumberFieldEditor.Dropdown)
{
yield return new ValidationError("Inline editing is only allowed for dropdowns and input fields.",
nameof(properties.InlineEditable),
nameof(properties.Editor));
}
}
public IEnumerable<ValidationError> Visit(ReferencesFieldProperties properties)
@ -216,6 +223,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
nameof(properties.MinLength),
nameof(properties.MaxLength));
}
if (properties.InlineEditable && properties.Editor != StringFieldEditor.Dropdown && properties.Editor != StringFieldEditor.Input && properties.Editor != StringFieldEditor.Slug)
{
yield return new ValidationError("Inline editing is only allowed for dropdowns, slugs and input fields.",
nameof(properties.InlineEditable),
nameof(properties.Editor));
}
}
public IEnumerable<ValidationError> Visit(TagsFieldProperties properties)

5
src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs

@ -21,6 +21,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields
/// </summary>
public bool? DefaultValue { get; set; }
/// <summary>
/// Indicates that the inline editor is enabled for this field.
/// </summary>
public bool InlineEditable { get; set; }
/// <summary>
/// The editor that is used to manage this field.
/// </summary>

5
src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs

@ -37,6 +37,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields
/// </summary>
public double[] AllowedValues { get; set; }
/// <summary>
/// Indicates that the inline editor is enabled for this field.
/// </summary>
public bool InlineEditable { get; set; }
/// <summary>
/// The editor that is used to manage this field.
/// </summary>

5
src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs

@ -47,6 +47,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields
/// </summary>
public string[] AllowedValues { get; set; }
/// <summary>
/// Indicates that the inline editor is enabled for this field.
/// </summary>
public bool InlineEditable { get; set; }
/// <summary>
/// The editor that is used to manage this field.
/// </summary>

4
src/Squidex/app/app.routes.ts

@ -6,7 +6,7 @@
*/
import { ModuleWithProviders } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
import { RouterModule, Routes } from '@angular/router';
import {
AppAreaComponent,
@ -96,4 +96,4 @@ export const routes: Routes = [
}
];
export const routing: ModuleWithProviders = RouterModule.forRoot(routes, { useHash: false, preloadingStrategy: PreloadAllModules });
export const routing: ModuleWithProviders = RouterModule.forRoot(routes, { useHash: false });

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

@ -5,112 +5,106 @@
<span class="field-disabled" *ngIf="field.isDisabled">Disabled</span>
<div [formGroup]="fieldForm">
<div *ngIf="field.partitioning === 'language' && languages.length > 1">
<div class="languages-buttons" #buttonLanguages>
<sqx-language-selector size="sm" (selectedLanguageChanged)="selectLanguage($event)" [languages]="languages"></sqx-language-selector>
</div>
<sqx-onboarding-tooltip id="languages" [for]="buttonLanguages" position="topRight" after="120000">
Please remember to check all languages when you see validation errors.
</sqx-onboarding-tooltip>
<div *ngIf="field.isLocalizable && languages.length > 1">
<div class="languages-buttons" #buttonLanguages>
<sqx-language-selector size="sm" (selectedLanguageChanged)="selectLanguage($event)" [languages]="languages"></sqx-language-selector>
</div>
<div *ngFor="let partition of fieldPartitions">
<div *ngIf="partition == fieldPartition">
<sqx-control-errors [for]="partition" fieldName="{{field.displayName}}" [submitted]="contentFormSubmitted"></sqx-control-errors>
<sqx-onboarding-tooltip id="languages" [for]="buttonLanguages" position="topRight" after="120000">
Please remember to check all languages when you see validation errors.
</sqx-onboarding-tooltip>
</div>
<div [ngSwitch]="field.properties.fieldType">
<div *ngSwitchCase="'Number'">
<div [ngSwitch]="field.properties.editor">
<div *ngSwitchCase="'Input'">
<input class="form-control" type="number" [formControlName]="partition" [placeholder]="field.properties.placeholder || ''" />
</div>
<div *ngSwitchCase="'Stars'">
<sqx-stars [formControlName]="partition" [maximumStars]="field.properties.maxValue"></sqx-stars>
</div>
<div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControlName]="partition">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.properties.allowedValues" [ngValue]="value">{{value}}</option>
</select>
</div>
<div *ngSwitchCase="'Radio'">
<div class="form-check form-check-inline" *ngFor="let value of field.properties.allowedValues">
<input class="form-check-input" type="radio" [value]="value" [formControlName]="partition" />
<label class="form-check-label">
{{value}}
</label>
</div>
</div>
</div>
</div>
<div *ngSwitchCase="'String'">
<div [ngSwitch]="field.properties.editor">
<div *ngSwitchCase="'Input'">
<input class="form-control" type="text" [formControlName]="partition" [placeholder]="field.properties.placeholder || ''" />
</div>
<div *ngSwitchCase="'Slug'">
<input class="form-control" type="text" [formControlName]="partition" [placeholder]="field.properties.placeholder || ''" sqxSlugifyInput />
</div>
<div *ngSwitchCase="'TextArea'">
<textarea class="form-control" [formControlName]="partition" rows="5" [placeholder]="field.properties.placeholder || ''"></textarea>
</div>
<div *ngSwitchCase="'RichText'">
<sqx-rich-editor [formControlName]="partition" (assetPluginClicked)="assetPluginClicked()"></sqx-rich-editor>
</div>
<div *ngSwitchCase="'Markdown'">
<sqx-markdown-editor [formControlName]="partition" (assetPluginClicked)="assetPluginClicked()"></sqx-markdown-editor>
</div>
<div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControlName]="partition">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.properties.allowedValues" [ngValue]="value">{{value}}</option>
</select>
</div>
<div *ngSwitchCase="'Radio'">
<div class="form-check form-check-inline" *ngFor="let value of field.properties.allowedValues">
<input class="form-check-input" type="radio" value="{{value}}" [formControlName]="partition" />
<label class="form-check-label">
{{value}}
</label>
</div>
</div>
</div>
</div>
<div *ngSwitchCase="'Boolean'">
<div [ngSwitch]="field.properties.editor">
<div *ngSwitchCase="'Toggle'">
<sqx-toggle [formControlName]="partition"></sqx-toggle>
</div>
<div *ngSwitchCase="'Checkbox'">
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" [formControlName]="partition" sqxIndeterminateValue />
</div>
</div>
</div>
</div>
<div *ngSwitchCase="'DateTime'">
<sqx-date-time-editor enforceTime="true" [mode]="field.properties.editor" [formControlName]="partition"></sqx-date-time-editor>
</div>
<div *ngSwitchCase="'Geolocation'">
<sqx-geolocation-editor [formControlName]="partition"></sqx-geolocation-editor>
</div>
<div *ngSwitchCase="'Json'">
<sqx-json-editor [formControlName]="partition"></sqx-json-editor>
</div>
<div *ngSwitchCase="'Assets'">
<sqx-assets-editor [formControlName]="partition"></sqx-assets-editor>
<sqx-control-errors [for]="selectedFormControl" [fieldName]="field.displayName" [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" [formControl]="selectedFormControl" [placeholder]="field.displayPlaceholder" />
</div>
<div *ngSwitchCase="'Stars'">
<sqx-stars [formControl]="selectedFormControl" [maximumStars]="field.properties.maxValue"></sqx-stars>
</div>
<div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControl]="selectedFormControl">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.properties.allowedValues" [ngValue]="value">{{value}}</option>
</select>
</div>
<div *ngSwitchCase="'Radio'">
<div class="form-check form-check-inline" *ngFor="let value of field.properties.allowedValues">
<input class="form-check-input" type="radio" [value]="value" [formControl]="selectedFormControl" />
<label class="form-check-label">
{{value}}
</label>
</div>
<div *ngSwitchCase="'Tags'">
<sqx-tag-editor [formControlName]="partition"></sqx-tag-editor>
</div>
</div>
</div>
<div *ngSwitchCase="'String'">
<div [ngSwitch]="field.properties.editor">
<div *ngSwitchCase="'Input'">
<input class="form-control" type="text" [formControl]="selectedFormControl" [placeholder]="field.displayPlaceholder" />
</div>
<div *ngSwitchCase="'Slug'">
<input class="form-control" type="text" [formControl]="selectedFormControl" [placeholder]="field.displayPlaceholder" sqxSlugifyInput />
</div>
<div *ngSwitchCase="'TextArea'">
<textarea class="form-control" [id]="selectedFormName"[formControl]="selectedFormControl" rows="5" [placeholder]="field.displayPlaceholder"></textarea>
</div>
<div *ngSwitchCase="'RichText'">
<sqx-rich-editor [formControl]="selectedFormControl" (assetPluginClicked)="assetPluginClicked()"></sqx-rich-editor>
</div>
<div *ngSwitchCase="'Markdown'">
<sqx-markdown-editor [formControl]="selectedFormControl" (assetPluginClicked)="assetPluginClicked()"></sqx-markdown-editor>
</div>
<div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControl]="selectedFormControl">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.properties.allowedValues" [ngValue]="value">{{value}}</option>
</select>
</div>
<div *ngSwitchCase="'Radio'">
<div class="form-check form-check-inline" *ngFor="let value of field.properties.allowedValues">
<input class="form-check-input" type="radio" value="{{value}}" [formControl]="selectedFormControl" />
<label class="form-check-label">
{{value}}
</label>
</div>
<div *ngSwitchCase="'References'">
<sqx-references-editor [formControlName]="partition" [languageCode]="selectFieldLanguage(partition)" [schemaId]="field.properties.schemaId"></sqx-references-editor>
</div>
</div>
</div>
<div *ngSwitchCase="'Boolean'">
<div [ngSwitch]="field.properties.editor">
<div *ngSwitchCase="'Toggle'">
<sqx-toggle [formControl]="selectedFormControl"></sqx-toggle>
</div>
<div *ngSwitchCase="'Checkbox'">
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" [formControl]="selectedFormControl" sqxIndeterminateValue />
</div>
</div>
</div>
</div>
<div *ngSwitchCase="'DateTime'">
<sqx-date-time-editor enforceTime="true" [mode]="field.properties.editor" [formControl]="selectedFormControl"></sqx-date-time-editor>
</div>
<div *ngSwitchCase="'Geolocation'">
<sqx-geolocation-editor [formControl]="selectedFormControl"></sqx-geolocation-editor>
</div>
<div *ngSwitchCase="'Json'">
<sqx-json-editor [formControl]="selectedFormControl"></sqx-json-editor>
</div>
<div *ngSwitchCase="'Assets'">
<sqx-assets-editor [formControl]="selectedFormControl"></sqx-assets-editor>
</div>
<div *ngSwitchCase="'Tags'">
<sqx-tag-editor [formControl]="selectedFormControl"></sqx-tag-editor>
</div>
<div *ngSwitchCase="'References'">
<sqx-references-editor [formControl]="selectedFormControl" [language]="selectedLanguage" [schemaId]="field.properties.schemaId"></sqx-references-editor>
</div>
</div>
<small class="form-text text-muted" *ngIf="field.properties.hints && field.properties.hints.length > 0">

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

@ -6,21 +6,21 @@
*/
import { Component, Input, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { AppLanguageDto, FieldDto } from 'shared';
import { AbstractControl, FormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import {
AppLanguageDto,
FieldDto,
fieldInvariant
} from 'shared';
@Component({
selector: 'sqx-content-field',
styleUrls: ['./content-field.component.scss'],
templateUrl: './content-field.component.html'
})
export class ContentFieldComponent implements OnInit {
constructor(private readonly router: Router, private readonly route: ActivatedRoute) {
}
private masterLanguageCode: string;
@Input()
public field: FieldDto;
@ -33,35 +33,36 @@ export class ContentFieldComponent implements OnInit {
@Input()
public contentFormSubmitted: boolean;
public fieldPartitions: string[];
public fieldPartition: string;
public selectedFormControl: AbstractControl;
public selectedLanguage: AppLanguageDto;
public selectLanguage(language: AppLanguageDto) {
this.fieldPartition = language.iso2Code;
constructor(
private readonly router: Router,
private readonly route: ActivatedRoute
) {
}
public ngOnInit() {
this.masterLanguageCode = this.languages.find(l => l.isMaster)!.iso2Code;
if (this.field.isDisabled) {
this.fieldForm.disable();
}
const masterLanguage = this.languages[0];
if (this.field.partitioning === 'language') {
this.fieldPartitions = this.languages.map(t => t.iso2Code);
this.fieldPartition = this.fieldPartitions[0];
if (this.field.isLocalizable) {
this.selectedFormControl = this.fieldForm.controls[masterLanguage.iso2Code];
} else {
this.fieldPartitions = ['iv'];
this.fieldPartition = 'iv';
this.selectedFormControl = this.fieldForm.controls[fieldInvariant];
}
this.selectedLanguage = masterLanguage;
}
public assetPluginClicked() {
this.router.navigate(['assets'], { relativeTo: this.route });
public selectLanguage(language: AppLanguageDto) {
this.selectedFormControl['_clearChangeFns']();
this.selectedFormControl = this.fieldForm.controls[language.iso2Code];
this.selectedLanguage = language;
}
public selectFieldLanguage(partition: string) {
return partition === 'iv' ? this.masterLanguageCode : partition;
public assetPluginClicked() {
this.router.navigate(['assets'], { relativeTo: this.route });
}
}

2
src/Squidex/app/features/content/pages/content/content-history.component.scss

@ -28,7 +28,7 @@
&-left {
min-width: 2.8rem;
max-width: 2.8rem;
margin-top: .3rem;
margin-top: .25rem;
}
&-created {

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

@ -25,6 +25,7 @@ import {
CanComponentDeactivate,
ContentDto,
ContentsService,
fieldInvariant,
SchemaDetailsDto,
Version
} from 'shared';
@ -40,6 +41,7 @@ import {
export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, OnInit {
private contentStatusChangedSubscription: Subscription;
private contentDeletedSubscription: Subscription;
private contentUpdatedSubscription: Subscription;
private contentVersionSelectedSubscription: Subscription;
public schema: SchemaDetailsDto;
@ -62,6 +64,7 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
public ngOnDestroy() {
this.contentVersionSelectedSubscription.unsubscribe();
this.contentStatusChangedSubscription.unsubscribe();
this.contentUpdatedSubscription.unsubscribe();
this.contentDeletedSubscription.unsubscribe();
}
@ -80,6 +83,14 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
}
});
this.contentUpdatedSubscription =
this.ctx.bus.of(ContentUpdated)
.subscribe(message => {
if (this.content && message.content.id === this.content.id) {
this.reloadContentForm(message.content);
}
});
this.contentStatusChangedSubscription =
this.ctx.bus.of(ContentStatusChanged)
.subscribe(message => {
@ -96,15 +107,12 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
const routeData = allData(this.ctx.route);
this.languages = routeData.appLanguages;
this.setupLanguages(routeData);
this.setupContentForm(routeData.schema);
this.ctx.route.data.map(d => d.content)
.subscribe((content: ContentDto) => {
this.content = content;
this.populateContentForm();
this.reloadContentForm(content);
});
}
@ -122,7 +130,7 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
this.contentOld = null;
this.emitContentUpdated(this.content);
this.populateContentForm();
this.reloadContentForm(this.content);
}
}
@ -159,13 +167,13 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
} else {
this.contentsService.putContent(this.ctx.appName, this.schema.name, this.content.id, requestDto, this.content.version)
.subscribe(dto => {
this.content = this.content.update(dto.payload, this.ctx.userToken, dto.version);
const content = this.content.update(dto.payload, this.ctx.userToken, dto.version);
this.ctx.notifyInfo('Content saved successfully.');
this.emitContentUpdated(this.content);
this.emitContentUpdated(content);
this.enableContentForm();
this.populateContentForm();
this.reloadContentForm(content);
}, error => {
this.ctx.notifyError(error);
@ -187,12 +195,9 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
this.contentOld = null;
}
this.content = this.content.setData(dto);
this.ctx.notifyInfo('Content version loaded successfully.');
this.emitContentUpdated(this.content);
this.populateContentForm();
this.reloadContentForm(this.content.setData(dto));
}, error => {
this.ctx.notifyError(error);
});
@ -221,35 +226,48 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
if (this.schema.fields.length === 0) {
this.contentForm.enable();
} else {
for (const field of this.schema.fields.filter(f => !f.isDisabled)) {
this.contentForm.controls[field.name].enable();
for (const field of this.schema.fields) {
const fieldForm = <FormGroup>this.contentForm.controls[field.name];
if (field.isDisabled) {
fieldForm.disable();
} else {
fieldForm.enable();
}
}
}
}
private setupLanguages(routeData: { [name: string]: any; }) {
this.languages = routeData.appLanguages;
}
private setupContentForm(schema: SchemaDetailsDto) {
this.schema = schema;
const controls: { [key: string]: AbstractControl } = {};
for (const field of schema.fields) {
const group = new FormGroup({});
const fieldForm = new FormGroup({});
if (field.partitioning === 'language') {
if (field.isLocalizable) {
for (let language of this.languages) {
group.addControl(language.iso2Code, new FormControl(undefined, field.createValidators(language.isOptional)));
fieldForm.setControl(language.iso2Code, new FormControl(undefined, field.createValidators(language.isOptional)));
}
} else {
group.addControl('iv', new FormControl(undefined, field.createValidators(false)));
fieldForm.setControl(fieldInvariant, new FormControl(undefined, field.createValidators(false)));
}
controls[field.name] = group;
controls[field.name] = fieldForm;
}
this.contentForm = new FormGroup(controls);
this.enableContentForm();
}
private populateContentForm() {
private reloadContentForm(content: ContentDto) {
this.content = content;
this.contentForm.markAsPristine();
this.isNewMode = !this.content;
@ -257,14 +275,14 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
if (!this.isNewMode) {
for (const field of this.schema.fields) {
const fieldValue = this.content.data[field.name] || {};
const fieldForm = <FormGroup>this.contentForm.get(field.name);
const fieldForm = <FormGroup>this.contentForm.controls[field.name];
if (field.partitioning === 'language') {
if (field.isLocalizable) {
for (let language of this.languages) {
fieldForm.controls[language.iso2Code].setValue(fieldValue[language.iso2Code]);
}
} else {
fieldForm.controls['iv'].setValue(fieldValue['iv'] === undefined ? null : fieldValue['iv']);
fieldForm.controls[fieldInvariant].setValue(fieldValue[fieldInvariant] === undefined ? null : fieldValue[fieldInvariant]);
}
}
if (this.content.status === 'Archived') {
@ -273,14 +291,16 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
} else {
for (const field of this.schema.fields) {
const defaultValue = field.defaultValue();
if (defaultValue) {
const fieldForm = <FormGroup>this.contentForm.get(field.name);
if (field.partitioning === 'language') {
const fieldForm = <FormGroup>this.contentForm.controls[field.name];
if (field.isLocalizable) {
for (let language of this.languages) {
fieldForm.controls[language.iso2Code].setValue(defaultValue);
}
} else {
fieldForm.controls['iv'].setValue(defaultValue);
fieldForm.controls[fieldInvariant].setValue(defaultValue);
}
}
}

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

@ -122,9 +122,9 @@
<div sqxIgnoreScrollbar>
<table class="table table-items table-fixed" *ngIf="contentItems" >
<tbody *ngIf="!isReadOnly">
<ng-template ngFor let-content [ngForOf]="contentItems">
<ng-template ngFor let-content [ngForOf]="contentItems" [ngForTrackBy]="trackBy">
<tr [sqxContent]="content" [routerLink]="[content.id]" routerLinkActive="active"
[languageCode]="languageSelected.iso2Code"
[language]="languageSelected"
[schemaFields]="contentFields"
[schema]="schema"
[selected]="isItemSelected(content)"
@ -133,7 +133,8 @@
(publishing)="publishContent(content)"
(archiving)="archiveContent(content)"
(restoring)="restoreContent(content)"
(deleting)="deleteContent(content)"></tr>
(deleting)="deleteContent(content)"
(saved)="onContentSaved(content, $event)"></tr>
<tr class="spacer"></tr>
</ng-template>
</tbody>
@ -141,7 +142,7 @@
<tbody *ngIf="isReadOnly">
<ng-template ngFor let-content [ngForOf]="contentItems">
<tr [sqxContent]="content" dnd-draggable [dragData]="dropData(content)"
[languageCode]="languageSelected.iso2Code"
[language]="languageSelected"
[schemaFields]="contentFields"
[schema]="schema"
isReadOnly="true"></tr>

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

@ -27,7 +27,8 @@ import {
ImmutableArray,
ModalView,
Pager,
SchemaDetailsDto
SchemaDetailsDto,
Versioned
} from 'shared';
@Component({
@ -203,7 +204,9 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
DateTime.parseISO_UTC(dueTime) :
null;
this.contentItems = this.contentItems.replaceBy('id', content.changeStatus(status, dt, this.ctx.userToken, dto.version));
content = content.changeStatus(status, dt, this.ctx.userToken, dto.version);
this.contentItems = this.contentItems.replaceBy('id', content);
this.emitContentStatusChanged(content);
}
@ -240,6 +243,14 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
});
}
public onContentSaved(content: ContentDto, update: Versioned<any>) {
content = content.update(update.payload, this.ctx.userToken, update.version);
this.contentItems = this.contentItems.replaceBy('id', content);
this.emitContentUpdated(content);
}
public load(showInfo = false) {
this.contentsService.getContents(this.ctx.appName, this.schema.name, this.contentsPager.pageSize, this.contentsPager.skip, this.contentsQuery, undefined, this.isArchive)
.finally(() => {
@ -342,10 +353,18 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
this.ctx.bus.emit(new ContentStatusChanged(content));
}
private emitContentUpdated(content: ContentDto) {
this.ctx.bus.emit(new ContentUpdated(content));
}
private emitContentRemoved(content: ContentDto) {
this.ctx.bus.emit(new ContentRemoved(content));
}
public trackBy(content: ContentDto): string {
return content.id;
}
private resetContents() {
this.contentItems = ImmutableArray.empty<ContentDto>();
this.contentsQuery = '';

79
src/Squidex/app/features/content/shared/content-item.component.html

@ -1,15 +1,61 @@
<td class="cell-select" *ngIf="!isReadOnly">
<input type="checkbox" class="form-control"
<td class="cell-select" *ngIf="!isReadOnly" (click)="shouldStop($event)">
<input type="checkbox" class="form-control"
[ngModel]="selected"
(ngModelChange)="selectedChange.emit($event);"
(click)="$event.stopPropagation()" />
</td>
<td class="cell-auto" *ngFor="let value of values">
<span class="table-cell">
{{value}}
</span>
<td class="cell-auto" *ngFor="let field of schemaFields; let i = index" (click)="shouldStop($event)">
<div *ngIf="field.properties.inlineEditable && !isReadOnly" [formGroup]="form" (click)="$event.stopPropagation()">
<div [ngSwitch]="field.properties.fieldType">
<div *ngSwitchCase="'Number'">
<div [ngSwitch]="field.properties.editor">
<div *ngSwitchCase="'Input'">
<input class="form-control" type="number" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" />
</div>
<div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControlName]="field.name">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.properties.allowedValues" [ngValue]="value">{{value}}</option>
</select>
</div>
</div>
</div>
<div *ngSwitchCase="'String'">
<div [ngSwitch]="field.properties.editor">
<div *ngSwitchCase="'Input'">
<input class="form-control" type="text" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" />
</div>
<div *ngSwitchCase="'Slug'">
<input class="form-control" type="text" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" sqxSlugifyInput />
</div>
<div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControlName]="field.name">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.properties.allowedValues" [ngValue]="value">{{value}}</option>
</select>
</div>
</div>
</div>
<div *ngSwitchCase="'Boolean'">
<div [ngSwitch]="field.properties.editor">
<div *ngSwitchCase="'Toggle'">
<sqx-toggle [formControlName]="field.name"></sqx-toggle>
</div>
<div *ngSwitchCase="'Checkbox'">
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" [formControlName]="field.name" sqxIndeterminateValue />
</div>
</div>
</div>
</div>
</div>
</div>
<div *ngIf="!field.properties.inlineEditable || isReadOnly" class="truncate">
{{values[i]}}
</div>
</td>
<td class="cell-time">
<td class="cell-time" (click)="shouldStop($event)">
<span *ngIf="!content.scheduledTo">
<span class="content-status content-status-{{content.status | lowercase}}" #statusIcon>
<i class="icon-circle"></i>
@ -28,10 +74,23 @@
<small class="item-modified">{{content.lastModified | sqxFromNow}}</small>
</td>
<td class="cell-user">
<td class="cell-user" *ngIf="form.dirty" (click)="shouldStop($event)">
<button type="button" class="btn btn-success" (click)="save(); $event.stopPropagation()">
<i class="icon-checkmark"></i>
</button>
</td>
<td class="cell-actions" *ngIf="form.dirty" (click)="shouldStop($event)">
<button type="button" class="btn btn-link btn-secondary btn-cancel" (click)="save(); $event.stopPropagation()">
<i class="icon-close"></i>
</button>
</td>
<td class="cell-user" *ngIf="form.pristine" (click)="shouldStop($event)">
<img class="user-picture" [attr.title]="content.lastModifiedBy | sqxUserNameRef" [attr.src]="content.lastModifiedBy | sqxUserPictureRef" />
</td>
<td class="cell-actions" *ngIf="!isReadOnly">
<td class="cell-actions" *ngIf="!isReadOnly && form.pristine" (click)="shouldStop($event)">
<div class="dropdown dropdown-options" *ngIf="content">
<button type="button" class="btn btn-link btn-secondary" (click)="dropdown.toggle(); $event.stopPropagation()" [class.active]="dropdown.isOpen | async" #optionsButton>
<i class="icon-dots"></i>
@ -58,7 +117,7 @@
</div>
</div>
</td>
<td class="cell-actions" *ngIf="isReference">
<td class="cell-actions" *ngIf="isReference" (click)="shouldStop($event)">
<button type="button" class="btn btn-link btn-secondary" (click)="deleting.emit(); $event.stopPropagation()">
<i class="icon-close"></i>
</button>

4
src/Squidex/app/features/content/shared/content-item.component.scss

@ -1,6 +1,10 @@
@import '_vars';
@import '_mixins';
.truncate {
@include truncate;
}
.content-status {
& {
vertical-align: middle;

106
src/Squidex/app/features/content/shared/content-item.component.ts

@ -5,15 +5,21 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import {
AppContext,
AppLanguageDto,
ContentDto,
ContentsService,
fadeAnimation,
FieldDto,
fieldInvariant,
ModalView,
SchemaDto
SchemaDto,
Types,
Versioned
} from 'shared';
/* tslint:disable:component-selector */
@ -27,8 +33,7 @@ import {
],
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush
]
})
export class ContentItemComponent implements OnInit, OnChanges {
@Output()
@ -46,6 +51,9 @@ export class ContentItemComponent implements OnInit, OnChanges {
@Output()
public deleting = new EventEmitter();
@Output()
public saved = new EventEmitter<Versioned<any>>();
@Output()
public selectedChange = new EventEmitter();
@ -53,10 +61,7 @@ export class ContentItemComponent implements OnInit, OnChanges {
public selected = false;
@Input()
public columnWidth: number;
@Input()
public languageCode: string;
public language: AppLanguageDto;
@Input()
public schemaFields: FieldDto[];
@ -73,11 +78,15 @@ export class ContentItemComponent implements OnInit, OnChanges {
@Input('sqxContent')
public content: ContentDto;
public formSubmitted = false;
public form: FormGroup = new FormGroup({});
public dropdown = new ModalView(false, true);
public values: any[] = [];
constructor(public readonly ctx: AppContext
constructor(public readonly ctx: AppContext,
private readonly contentsService: ContentsService
) {
}
@ -86,31 +95,96 @@ export class ContentItemComponent implements OnInit, OnChanges {
}
public ngOnInit() {
for (let field of this.schemaFields) {
if (field.properties['inlineEditable']) {
this.form.setControl(field.name, new FormControl(undefined, field.createValidators(this.language.isOptional)));
}
}
this.updateValues();
}
public shouldStop(event: Event) {
if (this.form.dirty) {
event.stopPropagation();
event.stopImmediatePropagation();
}
}
public save() {
this.formSubmitted = true;
if (this.form.dirty && this.form.valid) {
this.form.disable();
const request = {};
for (let field of this.schemaFields) {
if (field.properties['inlineEditable']) {
const value = this.form.controls[field.name].value;
if (field.isLocalizable) {
request[field.name] = { [this.language.iso2Code]: value };
} else {
request[field.name] = { iv: value };
}
}
}
this.contentsService.patchContent(this.ctx.appName, this.schema.name, this.content.id, request, this.content.version)
.finally(() => {
this.form.enable();
})
.subscribe(dto => {
this.form.markAsPristine();
this.emitSaved(dto);
}, error => {
this.ctx.notifyError(error);
});
}
}
private emitSaved(data: Versioned<any>) {
this.saved.emit(data);
}
private updateValues() {
this.values = [];
if (this.schemaFields) {
for (let field of this.schemaFields) {
this.values.push(this.getValue(field));
const value = this.getRawValue(field);
if (Types.isUndefined(value)) {
this.values.push('');
} else {
this.values.push(field.formatValue(value));
}
if (this.form) {
const formControl = this.form.controls[field.name];
if (formControl) {
formControl.setValue(value);
}
}
}
}
}
private getValue(field: FieldDto): any {
private getRawValue(field: FieldDto): any {
const contentField = this.content.data[field.name];
if (contentField) {
if (field.partitioning === 'language') {
return field.formatValue(contentField[this.languageCode]);
if (field.isLocalizable) {
return contentField[this.language.iso2Code];
} else {
return field.formatValue(contentField['iv']);
return contentField[fieldInvariant];
}
} else {
return '';
}
return undefined;
}
}

2
src/Squidex/app/features/content/shared/references-editor.component.html

@ -13,7 +13,7 @@
<tbody dnd-sortable-container [sortableData]="contentItems.mutableValues">
<ng-template ngFor let-content let-i="index" [ngForOf]="contentItems">
<tr [sqxContent]="content" dnd-sortable [sortableIndex]="i" (sqxSorted)="onContentsSorted($event)"
[languageCode]="languageCode"
[language]="language"
[schemaFields]="contentFields"
[schema]="schema"
(deleting)="onContentRemoving(content)"

2
src/Squidex/app/features/content/shared/references-editor.component.scss

@ -53,7 +53,7 @@
.table {
& {
margin-bottom: -.3rem;
margin-bottom: -.25rem;
margin-top: 1rem;
}

3
src/Squidex/app/features/content/shared/references-editor.component.ts

@ -12,6 +12,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import {
AppContext,
AppLanguageDto,
ContentDto,
ContentsService,
FieldDto,
@ -43,7 +44,7 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
public schemaId: string;
@Input()
public languageCode: string;
public language: AppLanguageDto;
public schema: SchemaDetailsDto;

2
src/Squidex/app/features/dashboard/pages/dashboard-page.component.scss

@ -136,7 +136,7 @@
&-left {
min-width: 2.8rem;
max-width: 2.8rem;
margin-top: .3rem;
margin-top: .25rem;
}
&-created {

2
src/Squidex/app/features/rules/pages/events/rule-events-page.component.scss

@ -35,7 +35,7 @@ h3 {
&::before {
@include caret-top($color-border);
@include absolute(-1.1rem, 2.5rem, auto, auto);
@include absolute(-1.1rem, 1.8rem, auto, auto);
}
h3 {

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

@ -6,7 +6,7 @@
<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-partitioning" *ngIf="field.partitioning === 'language'">localizable</span>
<span class="field-partitioning" *ngIf="field.isLocalizable">localizable</span>
</span>
</div>
<div class="col col-tags">

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

@ -4,7 +4,13 @@
$field-header: #e7ebef;
.table-items-row-details {
cursor: default;
& {
cursor: default;
}
&::before {
right: 4.6rem;
}
}
.col {

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

@ -13,6 +13,7 @@ import {
createProperties,
fadeAnimation,
FieldDto,
FieldPropertiesDto,
ModalView,
SchemaDto
} from 'shared';
@ -45,7 +46,7 @@ export class FieldComponent implements OnInit {
public showing = new EventEmitter();
@Output()
public saving = new EventEmitter();
public saving = new EventEmitter<FieldPropertiesDto>();
@Output()
public enabling = new EventEmitter();
@ -107,22 +108,12 @@ export class FieldComponent implements OnInit {
if (this.editForm.valid) {
const properties = createProperties(this.field.properties['fieldType'], this.editForm.value);
const field =
new FieldDto(
this.field.fieldId,
this.field.name,
this.field.isLocked,
this.field.isHidden,
this.field.isHidden,
this.field.partitioning,
properties);
this.emitSaving(field);
this.emitSaving(properties);
}
}
private emitSaving(field: FieldDto) {
this.saving.emit(field);
private emitSaving(properties: FieldPropertiesDto) {
this.saving.emit(properties);
}
private resetEditForm() {

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

@ -69,7 +69,7 @@
(locking)="lockField(field)"
(showing)="showField(field)"
(hiding)="hideField(field)"
(saving)="saveField($event)"></sqx-field>
(saving)="saveField(field, $event)"></sqx-field>
</div>
<div class="table-items-footer">

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

@ -25,7 +25,7 @@
color: $color-border-dark;
font-size: .9rem;
font-weight: normal;
padding: .6rem .3rem;
padding: .6rem .25rem;
border: 0;
background: transparent;
vertical-align: baseline;

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

@ -18,6 +18,7 @@ import {
createProperties,
fadeAnimation,
FieldDto,
FieldPropertiesDto,
fieldTypes,
HistoryChannelUpdated,
ImmutableArray,
@ -213,12 +214,12 @@ export class SchemaPageComponent implements OnDestroy, OnInit {
});
}
public saveField(field: FieldDto) {
const requestDto = new UpdateFieldDto(field.properties);
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, this.ctx.userToken, dto.version));
this.updateSchema(this.schema.updateField(field.update(properties), this.ctx.userToken, dto.version));
}, error => {
this.ctx.notifyError(error);
});

10
src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.html

@ -30,4 +30,14 @@
</label>
</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="fieldInlineEditable" formControlName="inlineEditable" />
<label class="form-check-label" for="fieldInlineEditable">
Inline Editable
</label>
</div>
</div>
</div>
</div>

8
src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.ts

@ -23,13 +23,17 @@ export class BooleanUIComponent implements OnInit {
public properties: BooleanFieldPropertiesDto;
public ngOnInit() {
this.editForm.addControl('editor',
this.editForm.setControl('editor',
new FormControl(this.properties.editor, [
Validators.required
]));
this.editForm.addControl('placeholder',
this.editForm.setControl('placeholder',
new FormControl(this.properties.placeholder, [
Validators.maxLength(100)
]));
this.editForm.setControl('inlineEditable',
new FormControl(this.properties.inlineEditable));
}
}

13
src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.html

@ -10,11 +10,14 @@
</div>
</div>
<div class="form-group row" *ngIf="showDefaultValue | async">
<label class="col col-3 col-form-checkbox-label" for="fieldDefaultValue">Default Value</label>
<div class="col col-6">
<input class="form-check-input" type="checkbox" id="fieldDefaultValue" formControlName="defaultValue" sqxIndeterminateValue />
<div class="form-group row">
<div class="col col-9 offset-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="fieldDefaultValue" formControlName="defaultValue" sqxIndeterminateValue />
<label class="form-check-label" for="fieldDefaultValue">
Default Value
</label>
</div>
</div>
</div>
</div>

5
src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.ts

@ -26,9 +26,12 @@ export class BooleanValidationComponent implements OnInit {
public showDefaultValue: Observable<boolean>;
public ngOnInit() {
this.editForm.addControl('defaultValue',
this.editForm.setControl('defaultValue',
new FormControl(this.properties.defaultValue));
this.editForm.setControl('inlineEditable',
new FormControl(this.properties.inlineEditable));
this.showDefaultValue =
this.editForm.controls['isRequired'].valueChanges
.startWith(this.properties.isRequired)

5
src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.ts

@ -28,11 +28,12 @@ export class DateTimeUIComponent implements OnInit {
public hideAllowedValues: Observable<boolean>;
public ngOnInit() {
this.editForm.addControl('editor',
this.editForm.setControl('editor',
new FormControl(this.properties.editor, [
Validators.required
]));
this.editForm.addControl('placeholder',
this.editForm.setControl('placeholder',
new FormControl(this.properties.placeholder, [
Validators.maxLength(100)
]));

8
src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.ts

@ -29,20 +29,20 @@ export class DateTimeValidationComponent implements OnInit {
public calculatedDefaultValues = ['Now', 'Today'];
public ngOnInit() {
this.editForm.addControl('calculatedDefaultValue',
this.editForm.setControl('calculatedDefaultValue',
new FormControl(this.properties.calculatedDefaultValue));
this.editForm.addControl('maxValue',
this.editForm.setControl('maxValue',
new FormControl(this.properties.maxValue, [
ValidatorsEx.validDateTime()
]));
this.editForm.addControl('minValue',
this.editForm.setControl('minValue',
new FormControl(this.properties.minValue, [
ValidatorsEx.validDateTime()
]));
this.editForm.addControl('defaultValue',
this.editForm.setControl('defaultValue',
new FormControl(this.properties.defaultValue, [
ValidatorsEx.validDateTime()
]));

2
src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.ts

@ -23,7 +23,7 @@ export class GeolocationUIComponent implements OnInit {
public properties: GeolocationFieldPropertiesDto;
public ngOnInit() {
this.editForm.addControl('editor',
this.editForm.setControl('editor',
new FormControl(this.properties.editor, [
Validators.required
]));

10
src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.html

@ -51,4 +51,14 @@
<sqx-tag-editor formControlName="allowedValues" [converter]="converter"></sqx-tag-editor>
</div>
</div>
<div class="form-group row" [class.hidden]="hideInlineEditable | async">
<div class="col col-9 offset-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="fieldInlineEditable" formControlName="inlineEditable" />
<label class="form-check-label" for="fieldInlineEditable">
Inline Editable
</label>
</div>
</div>
</div>
</div>

34
src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts

@ -17,7 +17,8 @@ import { FloatConverter, NumberFieldPropertiesDto } from 'shared';
templateUrl: 'number-ui.component.html'
})
export class NumberUIComponent implements OnDestroy, OnInit {
private editorSubscription: Subscription;
private hideAllowedValuesSubscription: Subscription;
private hideInlineEditableSubscription: Subscription;
@Input()
public editForm: FormGroup;
@ -28,33 +29,52 @@ export class NumberUIComponent implements OnDestroy, OnInit {
public converter = new FloatConverter();
public hideAllowedValues: Observable<boolean>;
public hideInlineEditable: Observable<boolean>;
public ngOnDestroy() {
this.editorSubscription.unsubscribe();
this.hideAllowedValuesSubscription.unsubscribe();
this.hideInlineEditableSubscription.unsubscribe();
}
public ngOnInit() {
this.editForm.addControl('editor',
this.editForm.setControl('editor',
new FormControl(this.properties.editor, [
Validators.required
]));
this.editForm.addControl('placeholder',
this.editForm.setControl('placeholder',
new FormControl(this.properties.placeholder, [
Validators.maxLength(100)
]));
this.editForm.addControl('allowedValues',
this.editForm.setControl('allowedValues',
new FormControl(this.properties.allowedValues, []));
this.editForm.setControl('inlineEditable',
new FormControl(this.properties.inlineEditable));
this.hideAllowedValues =
this.editForm.controls['editor'].valueChanges
.startWith(this.properties.editor)
.map(x => !x || x === 'Input' || x === 'Textarea');
.map(x => !(x && (x === 'Radio' || x === 'Dropdown')));
this.hideInlineEditable =
this.editForm.controls['editor'].valueChanges
.startWith(this.properties.editor)
.map(x => !(x && (x === 'Input' || x === 'Dropdown')));
this.editorSubscription =
this.hideAllowedValuesSubscription =
this.hideAllowedValues.subscribe(isSelection => {
if (isSelection) {
this.editForm.controls['allowedValues'].setValue(undefined);
}
});
this.hideInlineEditableSubscription =
this.hideInlineEditable.subscribe(isSelection => {
if (isSelection) {
this.editForm.controls['inlineEditable'].setValue(false);
}
});
}
}

6
src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.ts

@ -26,13 +26,13 @@ export class NumberValidationComponent implements OnInit {
public showDefaultValue: Observable<boolean>;
public ngOnInit() {
this.editForm.addControl('maxValue',
this.editForm.setControl('maxValue',
new FormControl(this.properties.maxValue));
this.editForm.addControl('minValue',
this.editForm.setControl('minValue',
new FormControl(this.properties.minValue));
this.editForm.addControl('defaultValue',
this.editForm.setControl('defaultValue',
new FormControl(this.properties.defaultValue));
this.showDefaultValue =

10
src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.html

@ -72,4 +72,14 @@
<sqx-tag-editor formControlName="allowedValues"></sqx-tag-editor>
</div>
</div>
<div class="form-group row" [class.hidden]="hideInlineEditable | async">
<div class="col col-9 offset-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="fieldInlineEditable" formControlName="inlineEditable" />
<label class="form-check-label" for="fieldInlineEditable">
Inline Editable
</label>
</div>
</div>
</div>
</div>

39
src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.ts

@ -17,7 +17,8 @@ import { StringFieldPropertiesDto } from 'shared';
templateUrl: 'string-ui.component.html'
})
export class StringUIComponent implements OnDestroy, OnInit {
private editorSubscription: Subscription;
private hideAllowedValuesSubscription: Subscription;
private hideInlineEditableSubscription: Subscription;
@Input()
public editForm: FormGroup;
@ -26,9 +27,11 @@ export class StringUIComponent implements OnDestroy, OnInit {
public properties: StringFieldPropertiesDto;
public hideAllowedValues: Observable<boolean>;
public hideInlineEditable: Observable<boolean>;
public ngOnDestroy() {
this.editorSubscription.unsubscribe();
this.hideAllowedValuesSubscription.unsubscribe();
this.hideInlineEditableSubscription.unsubscribe();
}
public ngOnInit() {
@ -45,17 +48,31 @@ export class StringUIComponent implements OnDestroy, OnInit {
this.editForm.setControl('allowedValues',
new FormControl(this.properties.allowedValues));
this.editForm.setControl('inlineEditable',
new FormControl(this.properties.inlineEditable));
this.hideAllowedValues =
this.editForm.controls['editor'].valueChanges
.startWith(this.properties.editor)
.map(x => !x || x === 'Input' || x === 'TextArea' || x === 'RichText' || x === 'Markdown');
this.editorSubscription =
this.hideAllowedValues
.subscribe(isSelection => {
if (isSelection) {
this.editForm.controls['allowedValues'].setValue(undefined);
}
});
.map(x => !(x && (x === 'Radio' || x === 'Dropdown')));
this.hideInlineEditable =
this.editForm.controls['editor'].valueChanges
.startWith(this.properties.editor)
.map(x => !(x && (x === 'Input' || x === 'Dropdown' || x === 'Slug')));
this.hideAllowedValuesSubscription =
this.hideAllowedValues.subscribe(isSelection => {
if (isSelection) {
this.editForm.controls['allowedValues'].setValue(undefined);
}
});
this.hideInlineEditableSubscription =
this.hideInlineEditable.subscribe(isSelection => {
if (isSelection) {
this.editForm.controls['inlineEditable'].setValue(false);
}
});
}
}

6
src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts

@ -64,14 +64,14 @@ export class StringValidationComponent implements OnDestroy, OnInit {
.startWith(this.properties.isRequired)
.map(x => !x);
this.showPatternMessage =
this.editForm.controls['pattern'].value && this.editForm.controls['pattern'].value.trim().length > 0;
this.showPatternSuggestions =
this.editForm.controls['pattern'].valueChanges
.startWith('')
.map(x => !x || x.trim().length === 0);
this.showPatternMessage =
this.editForm.controls['pattern'].value && this.editForm.controls['pattern'].value.trim().length > 0;
this.patternSubscription =
this.editForm.controls['pattern'].valueChanges
.subscribe((value: string) => {

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

@ -43,7 +43,7 @@ $button-size: calc(2.5rem - 2px);
@include truncate;
display: inline-block;
background: $color-dark2-control;
padding: .1rem .3rem;
padding: .1rem .25rem;
font-size: .8rem;
font-weight: normal;
margin-left: 10px;

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

@ -23,7 +23,7 @@ $color-editor: #eceeef;
display: none;
font-size: .9rem;
font-weight: normal;
padding: .6rem .3rem;
padding: .6rem .25rem;
border: 0;
background: transparent;
vertical-align: baseline;

8
src/Squidex/app/features/settings/pages/languages/languages-page.component.scss

@ -1,2 +1,8 @@
@import '_vars';
@import '_mixins';
@import '_mixins';
.table-item-row-details {
&::before {
right: 5.2rem;
}
}

2
src/Squidex/app/framework/angular/control-errors.component.html

@ -1,4 +1,4 @@
<div class="errors-container" *ngIf="errorMessages" @fade>
<div class="errors-container" *ngIf="errorMessages.length > 0" @fade>
<div class="errors">
<span *ngFor="let message of errorMessages">
{{message}}

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

@ -5,8 +5,9 @@
* Copyright (c) Sebastian Stehle. All rights r vbeserved
*/
import { ChangeDetectionStrategy, Component, Host, Input, OnChanges, Optional } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Host, Input, OnChanges, OnDestroy, Optional } from '@angular/core';
import { AbstractControl, FormGroupDirective } from '@angular/forms';
import { Observable, Subscription } from 'rxjs';
import { fadeAnimation } from './animations';
@ -33,12 +34,14 @@ const DEFAULT_ERRORS: { [key: string]: string } = {
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ControlErrorsComponent implements OnChanges {
export class ControlErrorsComponent implements OnChanges, OnDestroy {
private displayFieldName: string;
private control: AbstractControl;
private controlSubscription: Subscription | null = null;
private originalMarkAsTouched: any;
@Input()
public for: string;
public for: string | AbstractControl;
@Input()
public fieldName: string;
@ -52,17 +55,81 @@ export class ControlErrorsComponent implements OnChanges {
@Input()
public submitOnly = false;
public get errorMessages(): string[] | null {
if (!this.control) {
return null;
public errorMessages: string[] = [];
constructor(
@Optional() @Host() private readonly formGroupDirective: FormGroupDirective,
private readonly changeDetector: ChangeDetectorRef
) {
}
public ngOnDestroy() {
this.unsubscribe();
}
public ngOnChanges() {
if (this.fieldName) {
this.displayFieldName = this.fieldName;
} else if (this.for) {
if (this.for instanceof String) {
this.displayFieldName = this.for.substr(0, 1).toUpperCase() + this.for.substr(1);
} else {
this.displayFieldName = 'field';
}
}
let control: AbstractControl | null = null;
if (typeof this.for === 'string') {
control = this.formGroupDirective.form.controls[this.for];
} else {
control = this.for;
}
if (this.control !== control) {
this.unsubscribe();
this.control = control;
if (control) {
this.controlSubscription =
Observable.merge(control.valueChanges, control.statusChanges)
.subscribe(() => {
this.createMessages();
});
this.originalMarkAsTouched = this.control.markAsTouched;
const self = this;
this.control['markAsTouched'] = function () {
self.originalMarkAsTouched.apply(this, arguments);
self.createMessages();
};
}
}
if (this.control.invalid && ((this.control.touched && !this.submitOnly) || this.submitted) && this.control.errors) {
const errors: string[] = [];
this.createMessages();
}
private unsubscribe() {
if (this.controlSubscription) {
this.controlSubscription.unsubscribe();
}
if (this.control && this.originalMarkAsTouched) {
this.control['markAsTouched'] = this.originalMarkAsTouched;
}
}
private createMessages() {
const errors: string[] = [];
if (this.control && this.control.invalid && ((this.control.touched && !this.submitOnly) || this.submitted) && this.control.errors) {
for (let key in <any>this.control.errors) {
if (this.control.errors.hasOwnProperty(key)) {
let message = (this.errors ? this.errors[key] : null) || DEFAULT_ERRORS[key];
let message = (this.errors ? this.errors[key] : null) || DEFAULT_ERRORS[key.toLowerCase()];
if (!message) {
continue;
@ -81,28 +148,10 @@ export class ControlErrorsComponent implements OnChanges {
errors.push(message);
}
}
return errors.length > 0 ? errors : null;
}
return null;
}
constructor(
@Optional() @Host() private readonly formGroupDirective: FormGroupDirective
) {
if (!this.formGroupDirective) {
throw new Error('control-errors must be used with a parent formGroup directive');
}
}
public ngOnChanges() {
if (this.fieldName) {
this.displayFieldName = this.fieldName;
} else if (this.for) {
this.displayFieldName = this.for.substr(0, 1).toUpperCase() + this.for.substr(1);
}
this.errorMessages = errors;
this.control = this.formGroupDirective.form.controls[this.for];
this.changeDetector.detectChanges();
}
}

6
src/Squidex/app/framework/angular/http-extensions-impl.ts

@ -87,6 +87,12 @@ export module HTTP {
return handleVersion(http.put<T>(url, body, { observe: 'response', headers }), version);
}
export function patchVersioned<T>(http: HttpClient, url: string, body: any, version?: Version): Observable<Versioned<HttpResponse<T>>> {
const headers = createHeaders(version);
return handleVersion(http.request<T>('PATCH', url, { body, observe: 'response', headers }), version);
}
export function deleteVersioned<T>(http: HttpClient, url: string, version?: Version): Observable<Versioned<HttpResponse<T>>> {
const headers = createHeaders(version);

62
src/Squidex/app/framework/angular/modal-view.directive.ts

@ -21,7 +21,7 @@ export class ModalViewDirective implements OnChanges, OnDestroy {
private renderedView: EmbeddedViewRef<any> | null = null;
@Input('sqxModalView')
public modalView: ModalView;
public modalView: ModalView | any;
@Input('sqxModalViewOnRoot')
public placeOnRoot = false;
@ -40,7 +40,9 @@ export class ModalViewDirective implements OnChanges, OnDestroy {
public ngOnDestroy() {
this.stopListening();
this.modalView.hide();
if (this.modalView instanceof ModalView) {
this.modalView.hide();
}
}
public ngOnChanges(changes: SimpleChanges) {
@ -53,36 +55,42 @@ export class ModalViewDirective implements OnChanges, OnDestroy {
this.subscription = null;
}
if (this.modalView) {
if (this.modalView instanceof ModalView) {
this.subscription =
this.modalView.isOpen.subscribe(isOpen => {
if (isOpen === (this.renderedView !== null)) {
return;
}
this.update(isOpen);
});
} else {
this.update(!!this.modalView);
}
}
if (isOpen && !this.renderedView) {
if (this.placeOnRoot) {
this.renderedView = this.rootContainer.createEmbeddedView(this.templateRef);
} else {
this.renderedView = this.viewContainer.createEmbeddedView(this.templateRef);
}
this.renderer.setElementStyle(this.renderedView.rootNodes[0], 'display', 'block');
setTimeout(() => {
this.startListening();
});
} else if (!isOpen && this.renderedView) {
this.renderedView = null;
if (this.placeOnRoot) {
this.rootContainer.clear();
} else {
this.viewContainer.clear();
}
private update(isOpen: boolean) {
if (isOpen === (this.renderedView !== null)) {
return;
}
this.stopListening();
}
if (isOpen && !this.renderedView) {
if (this.placeOnRoot) {
this.renderedView = this.rootContainer.createEmbeddedView(this.templateRef);
} else {
this.renderedView = this.viewContainer.createEmbeddedView(this.templateRef);
}
this.renderer.setElementStyle(this.renderedView.rootNodes[0], 'display', 'block');
setTimeout(() => {
this.startListening();
});
} else if (!isOpen && this.renderedView) {
this.renderedView = null;
if (this.placeOnRoot) {
this.rootContainer.clear();
} else {
this.viewContainer.clear();
}
this.stopListening();
}
}

1
src/Squidex/app/framework/angular/stars.component.scss

@ -13,6 +13,7 @@ $color-gold: #ffd700;
border: 0;
height: 2.4rem;
line-height: 2.4rem;
vertical-align: middle;
}
&.disabled {

2
src/Squidex/app/framework/angular/tag-editor.component.scss

@ -12,7 +12,7 @@
display: inline-block;
color: $color-dark-foreground;
margin-right: .4rem;
margin-bottom: .3rem;
margin-bottom: .25rem;
min-height: 1.6rem;
padding: 0 .6rem;
background: $color-theme-blue;

4
src/Squidex/app/framework/angular/toggle.component.scss

@ -2,8 +2,8 @@
@import '_vars';
$toggle-width: 2.2rem;
$toggle-height: 1.4rem;
$toggle-button-size: $toggle-height - .3rem;
$toggle-height: 1.25rem;
$toggle-button-size: $toggle-height - .25rem;
.toggle {
&-button {

2
src/Squidex/app/framework/utils/string-helper.ts

@ -6,7 +6,7 @@
*/
export module StringHelper {
export function firstNonEmpty(...values: string[]) {
export function firstNonEmpty(...values: (string | undefined | null)[]) {
for (let value of values) {
if (value) {
value = value.trim();

4
src/Squidex/app/shared/components/asset.component.scss

@ -23,7 +23,7 @@
}
@mixin asset-type {
padding: .1rem .3rem;
padding: .1rem .25rem;
text-transform: uppercase;
font-size: .7rem;
font-weight: normal;
@ -62,7 +62,7 @@
&-text {
@include absolute(40%, 0, 0, 0);
text-align: center;
font-size: 1.3rem;
font-size: 1.25rem;
font-weight: lighter;
}
}

2
src/Squidex/app/shared/components/history.component.scss

@ -14,7 +14,7 @@
&-left {
min-width: 2.8rem;
max-width: 2.8rem;
margin-top: .3rem;
margin-top: .25rem;
}
&-created {

4
src/Squidex/app/shared/components/language-selector.component.html

@ -1,11 +1,11 @@
<div class="btn-group btn-group-{{size}}" *ngIf="isSmallMode">
<button type="button" class="btn btn-secondary" *ngFor="let language of languages" [attr.title]="language.englishName" [class.active]="language == selectedLanguage" (click)="selectLanguage(language)">
<button type="button" class="btn btn-secondary" *ngFor="let language of languages" [attr.title]="language.englishName" [class.active]="language == selectedLanguage" (click)="selectLanguage(language)" tabindex="-1">
<span class="iso-code">{{language.iso2Code}}</span>
</button>
</div>
<div class="dropdown-options btn-group btn-group-{{size}}" *ngIf="isLargeMode">
<button type="button" class="btn btn-secondary dropdown-toggle" [attr.title]="selectedLanguage.englishName" (click)="dropdown.toggle(); $event.stopPropagation()" #button>
<button type="button" class="btn btn-secondary dropdown-toggle" [attr.title]="selectedLanguage.englishName" (click)="dropdown.toggle(); $event.stopPropagation()" #button tabindex="-1">
{{selectedLanguage.iso2Code}}
</button>
<div class="dropdown-menu" *sqxModalView="dropdown" [sqxModalTarget]="button" @fade>

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

@ -312,6 +312,21 @@ describe('ContentsService', () => {
req.flush({});
}));
it('should make patch request to update content',
inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => {
const dto = {};
contentsService.patchContent('my-app', 'my-schema', 'content1', dto, version).subscribe();
const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/content1');
expect(req.request.method).toEqual('PATCH');
expect(req.request.headers.get('If-Match')).toBe(version.value);
req.flush({});
}));
it('should make put request to change content status',
inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => {

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

@ -239,6 +239,21 @@ export class ContentsService {
.pretifyError('Failed to update content. Please reload.');
}
public patchContent(appName: string, schemaName: string, id: string, dto: any, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}`);
return HTTP.patchVersioned(this.http, url, dto, version)
.map(response => {
const body = response.payload.body;
return new Versioned(response.version, body);
})
.do(() => {
this.analytics.trackEvent('Content', 'Updated', appName);
})
.pretifyError('Failed to update content. Please reload.');
}
public deleteContent(appName: string, schemaName: string, id: string, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}`);

6
src/Squidex/app/shared/services/schemas.fields.spec.ts

@ -117,7 +117,7 @@ describe('TagsField', () => {
});
describe('BooleanField', () => {
const field = createField(new BooleanFieldPropertiesDto(null, null, null, true, false, 'Checkbox'));
const field = createField(new BooleanFieldPropertiesDto(null, null, null, true, false, false, 'Checkbox'));
it('should create validators', () => {
expect(field.createValidators(false).length).toBe(1);
@ -230,7 +230,7 @@ describe('JsonField', () => {
});
describe('NumberField', () => {
const field = createField(new NumberFieldPropertiesDto(null, null, null, true, false, 'Input', undefined, 3, 1, [1, 2, 3]));
const field = createField(new NumberFieldPropertiesDto(null, null, null, true, false, false, 'Input', undefined, 3, 1, [1, 2, 3]));
it('should create validators', () => {
expect(field.createValidators(false).length).toBe(4);
@ -276,7 +276,7 @@ describe('ReferencesField', () => {
});
describe('StringField', () => {
const field = createField(new StringFieldPropertiesDto(null, null, null, true, false, 'Input', undefined, 'pattern', undefined, 3, 1, ['1', '2']));
const field = createField(new StringFieldPropertiesDto(null, null, null, true, false, false, 'Input', undefined, 'pattern', undefined, 3, 1, ['1', '2']));
it('should create validators', () => {
expect(field.createValidators(false).length).toBe(5);

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

@ -35,18 +35,20 @@ export const fieldTypes: string[] = [
'Tags'
];
export const fieldInvariant = 'iv';
export function createProperties(fieldType: string, values: Object | null = null): FieldPropertiesDto {
let properties: FieldPropertiesDto;
switch (fieldType) {
case 'Number':
properties = new NumberFieldPropertiesDto(null, null, null, false, false, 'Input');
properties = new NumberFieldPropertiesDto(null, null, null, false, false, false, 'Input');
break;
case 'String':
properties = new StringFieldPropertiesDto(null, null, null, false, false, 'Input');
properties = new StringFieldPropertiesDto(null, null, null, false, false, false, 'Input');
break;
case 'Boolean':
properties = new BooleanFieldPropertiesDto(null, null, null, false, false, 'Checkbox');
properties = new BooleanFieldPropertiesDto(null, null, null, false, false, false, 'Checkbox');
break;
case 'DateTime':
properties = new DateTimeFieldPropertiesDto(null, null, null, false, false, 'DateTime');
@ -78,9 +80,7 @@ export function createProperties(fieldType: string, values: Object | null = null
}
export class SchemaDto {
public get displayName() {
return StringHelper.firstNonEmpty(this.properties.label || '', this.name);
}
public readonly displayName = StringHelper.firstNonEmpty(this.properties.label, this.name);
constructor(
public readonly id: string,
@ -279,9 +279,10 @@ export class SchemaDetailsDto extends SchemaDto {
}
export class FieldDto {
public get displayName() {
return StringHelper.firstNonEmpty(this.properties.label || '', this.name);
}
public readonly displayName = StringHelper.firstNonEmpty(this.properties.label, this.name);
public readonly displayPlaceholder = this.properties.placeholder || '';
public readonly isLocalizable = this.partitioning !== 'invariant';
constructor(
public readonly fieldId: number,
@ -355,6 +356,7 @@ export class StringFieldPropertiesDto extends FieldPropertiesDto {
constructor(label: string | null, hints: string | null, placeholder: string | null,
isRequired: boolean,
isListField: boolean,
public readonly inlineEditable: boolean,
public readonly editor: string,
public readonly defaultValue?: string,
public readonly pattern?: string,
@ -415,6 +417,7 @@ export class NumberFieldPropertiesDto extends FieldPropertiesDto {
constructor(label: string | null, hints: string | null, placeholder: string | null,
isRequired: boolean,
isListField: boolean,
public readonly inlineEditable: boolean,
public readonly editor: string,
public readonly defaultValue?: number,
public readonly maxValue?: number,
@ -523,6 +526,7 @@ export class BooleanFieldPropertiesDto extends FieldPropertiesDto {
constructor(label: string | null, hints: string | null, placeholder: string | null,
isRequired: boolean,
isListField: boolean,
public readonly inlineEditable: boolean,
public readonly editor: string,
public readonly defaultValue?: boolean
) {

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

@ -10,7 +10,7 @@
& {
font-size: .9rem;
font-weight: normal;
padding: .3rem .6rem;
padding: .25rem .6rem;
}
&-primary {
@ -421,7 +421,7 @@ a {
}
&-header {
@include border-radius-top(.3rem);
@include border-radius-top(.25rem);
background: $color-modal-header-background;
font-size: 1rem;
font-weight: normal;

4
src/Squidex/app/theme/_forms.scss

@ -48,7 +48,7 @@
font-size: .9rem;
font-weight: normal;
line-height: 1.1rem;
padding: .3rem .4rem;
padding: .25rem .4rem;
padding-bottom: .5rem;
background: $color-theme-error;
}
@ -100,7 +100,7 @@
max-height: 15rem;
border: 1px solid $color-input-border;
background: $color-dark-foreground;
padding: .3rem 0;
padding: .25rem 0;
overflow-y: auto;
}

4
src/Squidex/app/theme/_lists.scss

@ -127,7 +127,7 @@
// Caret that is placed next to the expand button.
&::before {
@include caret-top($color-border);
@include absolute(-1.1rem, 5.2rem, auto, auto);
@include absolute(-1.1rem, 0, auto, auto);
}
&-tab {
@ -177,7 +177,7 @@
// Spacer item between two normal items, because table rows cannot have margins.
.spacer {
border: 0;
height: .3rem;
height: .25rem;
}
}

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

@ -109,7 +109,7 @@ noscript {
&-icon {
display: inline-block;
font-size: 1.3rem;
font-size: 1.25rem;
font-weight: normal;
vertical-align: middle;
width: 1.6rem;

28
tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs

@ -129,5 +129,33 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards.FieldProperties
new ValidationError("Editor is not a valid value.", "Editor")
});
}
[Theory]
[InlineData(NumberFieldEditor.Radio)]
[InlineData(NumberFieldEditor.Stars)]
public void Should_add_error_if_inline_editing_is_not_allowed_for_editor(NumberFieldEditor editor)
{
var sut = new NumberFieldProperties { InlineEditable = true, Editor = editor, AllowedValues = ImmutableList.Create(1.0) };
var errors = FieldPropertiesValidator.Validate(sut).ToList();
errors.ShouldBeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Inline editing is only allowed for dropdowns and input fields.", "InlineEditable", "Editor")
});
}
[Theory]
[InlineData(NumberFieldEditor.Input)]
[InlineData(NumberFieldEditor.Dropdown)]
public void Should_not_add_error_if_inline_editing_is_allowed_for_editor(NumberFieldEditor editor)
{
var sut = new NumberFieldProperties { InlineEditable = true, Editor = editor, AllowedValues = ImmutableList.Create(1.0) };
var errors = FieldPropertiesValidator.Validate(sut).ToList();
Assert.Empty(errors);
}
}
}

31
tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs

@ -100,5 +100,36 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards.FieldProperties
new ValidationError("Pattern is not a valid expression.", "Pattern")
});
}
[Theory]
[InlineData(StringFieldEditor.Markdown)]
[InlineData(StringFieldEditor.Radio)]
[InlineData(StringFieldEditor.RichText)]
[InlineData(StringFieldEditor.TextArea)]
public void Should_add_error_if_inline_editing_is_not_allowed_for_editor(StringFieldEditor editor)
{
var sut = new StringFieldProperties { InlineEditable = true, Editor = editor, AllowedValues = ImmutableList.Create("Value") };
var errors = FieldPropertiesValidator.Validate(sut).ToList();
errors.ShouldBeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Inline editing is only allowed for dropdowns, slugs and input fields.", "InlineEditable", "Editor")
});
}
[Theory]
[InlineData(StringFieldEditor.Dropdown)]
[InlineData(StringFieldEditor.Input)]
[InlineData(StringFieldEditor.Slug)]
public void Should_not_add_error_if_inline_editing_is_allowed_for_editor(StringFieldEditor editor)
{
var sut = new StringFieldProperties { InlineEditable = true, Editor = editor, AllowedValues = ImmutableList.Create("Value") };
var errors = FieldPropertiesValidator.Validate(sut).ToList();
Assert.Empty(errors);
}
}
}

Loading…
Cancel
Save