diff --git a/Dockerfile b/Dockerfile index be4c10abb..3895f5fcf 100644 --- a/Dockerfile +++ b/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 diff --git a/libs/Dockerfile b/libs/Dockerfile new file mode 100644 index 000000000..c10edc3b2 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/libs/keep b/libs/keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs index 2785eceba..531507e37 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs +++ b/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(IFieldPropertiesVisitor visitor) diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs index bd86a2a74..5732601a9 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs +++ b/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(IFieldPropertiesVisitor visitor) diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs index 008c18c58..df46bdf6d 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs +++ b/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; } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs index 75db6b529..49193c29d 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs +++ b/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"; diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/ElasticSearchActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/ElasticSearchActionHandler.cs index 53b43edeb..5ef0359bc 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/ElasticSearchActionHandler.cs +++ b/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(); diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs index 44becd941..e746580f9 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs +++ b/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 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 Visit(TagsFieldProperties properties) diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs index 2f7552b87..45964540e 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs @@ -21,6 +21,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields /// public bool? DefaultValue { get; set; } + /// + /// Indicates that the inline editor is enabled for this field. + /// + public bool InlineEditable { get; set; } + /// /// The editor that is used to manage this field. /// diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs index 4efdf18e1..3a255891c 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs @@ -37,6 +37,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields /// public double[] AllowedValues { get; set; } + /// + /// Indicates that the inline editor is enabled for this field. + /// + public bool InlineEditable { get; set; } + /// /// The editor that is used to manage this field. /// diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs index 5cae9dfb9..4451846ca 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs @@ -47,6 +47,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields /// public string[] AllowedValues { get; set; } + /// + /// Indicates that the inline editor is enabled for this field. + /// + public bool InlineEditable { get; set; } + /// /// The editor that is used to manage this field. /// diff --git a/src/Squidex/app/app.routes.ts b/src/Squidex/app/app.routes.ts index 00674bff9..c6ca613b6 100644 --- a/src/Squidex/app/app.routes.ts +++ b/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 }); \ No newline at end of file +export const routing: ModuleWithProviders = RouterModule.forRoot(routes, { useHash: false }); \ No newline at end of file diff --git a/src/Squidex/app/features/content/pages/content/content-field.component.html b/src/Squidex/app/features/content/pages/content/content-field.component.html index 7a05664f4..7e4a1e9f6 100644 --- a/src/Squidex/app/features/content/pages/content/content-field.component.html +++ b/src/Squidex/app/features/content/pages/content/content-field.component.html @@ -5,112 +5,106 @@ Disabled -
-
-
- -
- - - Please remember to check all languages when you see validation errors. - +
+
+
-
-
- + + Please remember to check all languages when you see validation errors. + +
-
-
-
-
- -
-
- -
-
- -
-
-
- - -
-
-
-
-
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
- - -
-
-
-
-
-
-
- -
-
-
- -
-
-
-
-
- -
-
- -
-
- -
-
- + + +
+
+
+
+ +
+
+ +
+
+ +
+
+
+ +
-
- +
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ +
-
- +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
diff --git a/src/Squidex/app/features/content/pages/content/content-field.component.ts b/src/Squidex/app/features/content/pages/content/content-field.component.ts index 4152da206..deca02d33 100644 --- a/src/Squidex/app/features/content/pages/content/content-field.component.ts +++ b/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 }); } } diff --git a/src/Squidex/app/features/content/pages/content/content-history.component.scss b/src/Squidex/app/features/content/pages/content/content-history.component.scss index 161a7830b..9fa38efc6 100644 --- a/src/Squidex/app/features/content/pages/content/content-history.component.scss +++ b/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 { diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.ts b/src/Squidex/app/features/content/pages/content/content-page.component.ts index 9ba93a9e9..fceb55df8 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.ts +++ b/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 = 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 = this.contentForm.get(field.name); + const fieldForm = 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 = this.contentForm.get(field.name); - if (field.partitioning === 'language') { + const fieldForm = 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); } } } diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.html b/src/Squidex/app/features/content/pages/contents/contents-page.component.html index f00ddc40a..388999bbb 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-page.component.html +++ b/src/Squidex/app/features/content/pages/contents/contents-page.component.html @@ -122,9 +122,9 @@
- + + (deleting)="deleteContent(content)" + (saved)="onContentSaved(content, $event)"> @@ -141,7 +142,7 @@ diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts b/src/Squidex/app/features/content/pages/contents/contents-page.component.ts index 689d817ec..7f984b731 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts +++ b/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) { + 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(); this.contentsQuery = ''; diff --git a/src/Squidex/app/features/content/shared/content-item.component.html b/src/Squidex/app/features/content/shared/content-item.component.html index 051ed7162..ccc5f9ba0 100644 --- a/src/Squidex/app/features/content/shared/content-item.component.html +++ b/src/Squidex/app/features/content/shared/content-item.component.html @@ -1,15 +1,61 @@ - - - - + + + - - {{displayName}} - localizable + localizable
diff --git a/src/Squidex/app/features/schemas/pages/schema/field.component.scss b/src/Squidex/app/features/schemas/pages/schema/field.component.scss index cd74d0355..2a8404a4b 100644 --- a/src/Squidex/app/features/schemas/pages/schema/field.component.scss +++ b/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 { diff --git a/src/Squidex/app/features/schemas/pages/schema/field.component.ts b/src/Squidex/app/features/schemas/pages/schema/field.component.ts index d252a0946..c8ff4d8bd 100644 --- a/src/Squidex/app/features/schemas/pages/schema/field.component.ts +++ b/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(); @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() { diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html index 6cefc740c..26476dd4e 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html +++ b/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)"> + (saving)="saveField(field, $event)">
+
+
+
+ + +
+
+
\ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.ts index 5eb3aadcd..7336ef71a 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.ts +++ b/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)); } } \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.html b/src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.html index 6a68e0180..f90e71d7f 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.html @@ -10,11 +10,14 @@ -
- - -
- +
+
+
+ + +
\ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.ts index b6839be87..7dbc279be 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.ts @@ -26,9 +26,12 @@ export class BooleanValidationComponent implements OnInit { public showDefaultValue: Observable; 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) diff --git a/src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.ts index 5a5152f6e..97a0855fd 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.ts +++ b/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; 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) ])); diff --git a/src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.ts index 15f133f45..4ff7d6be2 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.ts +++ b/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() ])); diff --git a/src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.ts index 14883cf52..a5de1bf68 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.ts +++ b/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 ])); diff --git a/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.html b/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.html index 3dc5ae6cf..fba63c8d1 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.html @@ -51,4 +51,14 @@
+
+
+
+ + +
+
+
\ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts index f63f9dac5..30342c35d 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts +++ b/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; + public hideInlineEditable: Observable; 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); + } + }); } } \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.ts index e8801a593..6e076e127 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.ts @@ -26,13 +26,13 @@ export class NumberValidationComponent implements OnInit { public showDefaultValue: Observable; 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 = diff --git a/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.html b/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.html index 4114db13d..d64264d0d 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.html @@ -72,4 +72,14 @@ +
+
+
+ + +
+
+
\ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.ts index d11c4effd..157b66cfe 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.ts +++ b/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; + public hideInlineEditable: Observable; 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); + } + }); } } \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts index c7e593132..fde5f8edf 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts +++ b/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) => { diff --git a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.scss b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.scss index 7d5f586d9..7927c3637 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.scss +++ b/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; diff --git a/src/Squidex/app/features/settings/pages/clients/client.component.scss b/src/Squidex/app/features/settings/pages/clients/client.component.scss index 5bfbabd3f..6dfdfcfa1 100644 --- a/src/Squidex/app/features/settings/pages/clients/client.component.scss +++ b/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; diff --git a/src/Squidex/app/features/settings/pages/languages/languages-page.component.scss b/src/Squidex/app/features/settings/pages/languages/languages-page.component.scss index fbb752506..22ab448f1 100644 --- a/src/Squidex/app/features/settings/pages/languages/languages-page.component.scss +++ b/src/Squidex/app/features/settings/pages/languages/languages-page.component.scss @@ -1,2 +1,8 @@ @import '_vars'; -@import '_mixins'; \ No newline at end of file +@import '_mixins'; + +.table-item-row-details { + &::before { + right: 5.2rem; + } +} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/control-errors.component.html b/src/Squidex/app/framework/angular/control-errors.component.html index 6468b69d6..5cab06ca2 100644 --- a/src/Squidex/app/framework/angular/control-errors.component.html +++ b/src/Squidex/app/framework/angular/control-errors.component.html @@ -1,4 +1,4 @@ -
+
{{message}} diff --git a/src/Squidex/app/framework/angular/control-errors.component.ts b/src/Squidex/app/framework/angular/control-errors.component.ts index 9f1f9c972..df6fa3819 100644 --- a/src/Squidex/app/framework/angular/control-errors.component.ts +++ b/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 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(); } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/http-extensions-impl.ts b/src/Squidex/app/framework/angular/http-extensions-impl.ts index 3b96117ec..f99f383b7 100644 --- a/src/Squidex/app/framework/angular/http-extensions-impl.ts +++ b/src/Squidex/app/framework/angular/http-extensions-impl.ts @@ -87,6 +87,12 @@ export module HTTP { return handleVersion(http.put(url, body, { observe: 'response', headers }), version); } + export function patchVersioned(http: HttpClient, url: string, body: any, version?: Version): Observable>> { + const headers = createHeaders(version); + + return handleVersion(http.request('PATCH', url, { body, observe: 'response', headers }), version); + } + export function deleteVersioned(http: HttpClient, url: string, version?: Version): Observable>> { const headers = createHeaders(version); diff --git a/src/Squidex/app/framework/angular/modal-view.directive.ts b/src/Squidex/app/framework/angular/modal-view.directive.ts index a8df8263b..fea01ae0b 100644 --- a/src/Squidex/app/framework/angular/modal-view.directive.ts +++ b/src/Squidex/app/framework/angular/modal-view.directive.ts @@ -21,7 +21,7 @@ export class ModalViewDirective implements OnChanges, OnDestroy { private renderedView: EmbeddedViewRef | 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(); } } diff --git a/src/Squidex/app/framework/angular/stars.component.scss b/src/Squidex/app/framework/angular/stars.component.scss index f518a8eee..1c8c6c5f1 100644 --- a/src/Squidex/app/framework/angular/stars.component.scss +++ b/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 { diff --git a/src/Squidex/app/framework/angular/tag-editor.component.scss b/src/Squidex/app/framework/angular/tag-editor.component.scss index bdb2af557..024cc5ca8 100644 --- a/src/Squidex/app/framework/angular/tag-editor.component.scss +++ b/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; diff --git a/src/Squidex/app/framework/angular/toggle.component.scss b/src/Squidex/app/framework/angular/toggle.component.scss index d3c6432b6..580f9f16c 100644 --- a/src/Squidex/app/framework/angular/toggle.component.scss +++ b/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 { diff --git a/src/Squidex/app/framework/utils/string-helper.ts b/src/Squidex/app/framework/utils/string-helper.ts index 06cd2cee8..778da4c2a 100644 --- a/src/Squidex/app/framework/utils/string-helper.ts +++ b/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(); diff --git a/src/Squidex/app/shared/components/asset.component.scss b/src/Squidex/app/shared/components/asset.component.scss index 147c38474..257ec6bac 100644 --- a/src/Squidex/app/shared/components/asset.component.scss +++ b/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; } } diff --git a/src/Squidex/app/shared/components/history.component.scss b/src/Squidex/app/shared/components/history.component.scss index 5e93d36ed..561b534d7 100644 --- a/src/Squidex/app/shared/components/history.component.scss +++ b/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 { diff --git a/src/Squidex/app/shared/components/language-selector.component.html b/src/Squidex/app/shared/components/language-selector.component.html index 880e57172..466bc2802 100644 --- a/src/Squidex/app/shared/components/language-selector.component.html +++ b/src/Squidex/app/shared/components/language-selector.component.html @@ -1,11 +1,11 @@
-
- + - - {{value}} - + + +
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+ {{values[i]}} +
+ @@ -28,10 +74,23 @@ {{content.lastModified | sqxFromNow}} + + + + + + + + + diff --git a/src/Squidex/app/features/content/shared/content-item.component.scss b/src/Squidex/app/features/content/shared/content-item.component.scss index d87c980ec..a9239ad91 100644 --- a/src/Squidex/app/features/content/shared/content-item.component.scss +++ b/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; diff --git a/src/Squidex/app/features/content/shared/content-item.component.ts b/src/Squidex/app/features/content/shared/content-item.component.ts index d5f667beb..794b20523 100644 --- a/src/Squidex/app/features/content/shared/content-item.component.ts +++ b/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>(); + @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) { + 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; } } diff --git a/src/Squidex/app/features/content/shared/references-editor.component.html b/src/Squidex/app/features/content/shared/references-editor.component.html index 7e2a68453..07147915e 100644 --- a/src/Squidex/app/features/content/shared/references-editor.component.html +++ b/src/Squidex/app/features/content/shared/references-editor.component.html @@ -13,7 +13,7 @@