diff --git a/backend/i18n/source/backend_en.json b/backend/i18n/source/backend_en.json index 98edba8ac..5d154fc31 100644 --- a/backend/i18n/source/backend_en.json +++ b/backend/i18n/source/backend_en.json @@ -142,6 +142,7 @@ "contents.singletonNotChangeable": "Singleton content cannot be updated.", "contents.singletonNotCreatable": "Singleton content cannot be created.", "contents.singletonNotDeletable": "Singleton content cannot be deleted.", + "contents.componentNotCreatable": "Component content cannot be created.", "contents.statusNotValid": "Status is not defined in the workflow.", "contents.statusTransitionNotAllowed": "Cannot change status from {oldStatus} to {newStatus}.", "contents.validation.aspectRatio": "Must have aspect ratio {width}:{height}.", diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs index 9d5122c6e..3f27f58fc 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs @@ -228,6 +228,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject private async Task CreateCore(CreateContent c, ContentOperation operation) { + operation.MustNotCreateComponent(); operation.MustNotCreateSingleton(); operation.MustNotCreateForUnpublishedSchema(); operation.MustHaveData(c.Data); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/SingletonExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/SingletonExtensions.cs index 61b3cfa4c..bb4354e28 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/SingletonExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/SingletonExtensions.cs @@ -22,6 +22,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards } } + public static void MustNotCreateComponent(this ContentOperation operation) + { + if (operation.SchemaDef.Type == SchemaType.Component) + { + throw new DomainException(T.Get("contents.componentNotCreatable")); + } + } + public static void MustNotCreateSingleton(this ContentOperation operation) { if (operation.SchemaDef.Type == SchemaType.Singleton && operation.CommandId != operation.Schema.Id) diff --git a/backend/src/Squidex.Infrastructure/Tasks/Scheduler.cs b/backend/src/Squidex.Infrastructure/Tasks/Scheduler.cs index 0dbbdc1df..b061b7d4a 100644 --- a/backend/src/Squidex.Infrastructure/Tasks/Scheduler.cs +++ b/backend/src/Squidex.Infrastructure/Tasks/Scheduler.cs @@ -113,6 +113,8 @@ namespace Squidex.Infrastructure.Tasks } finally { + semaphore.Release(); + if (Interlocked.Decrement(ref pendingTasks) <= 1) { tcs.TrySetResult(true); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs index 0c36075ae..8d16d78b0 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs @@ -154,12 +154,12 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models AddSelfLink(resources.Url(x => nameof(x.GetSchema), values)); - if (resources.CanReadContent(Name)) + if (resources.CanReadContent(Name) && Type == SchemaType.Default) { AddGetLink("contents", resources.Url(x => nameof(x.GetContents), values)); } - if (resources.CanCreateContent(Name)) + if (resources.CanCreateContent(Name) && Type == SchemaType.Default) { AddPostLink("contents/create", resources.Url(x => nameof(x.PostContent), values)); AddPostLink("contents/create/publish", resources.Url(x => nameof(x.PostContent), values) + "?publish=true"); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs index 76aad2981..4f8c7ee94 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs @@ -24,7 +24,7 @@ namespace Squidex.Areas.Api.Controllers.Translations.Models public static TranslationDto FromDomain(TranslationResult translation) { - return SimpleMapper.Map(translation, new TranslationDto()); + return SimpleMapper.Map(translation, new TranslationDto { Result = translation.Code }); } } } diff --git a/backend/src/Squidex/Config/Domain/BackupsServices.cs b/backend/src/Squidex/Config/Domain/BackupsServices.cs index d0494568d..2b214af00 100644 --- a/backend/src/Squidex/Config/Domain/BackupsServices.cs +++ b/backend/src/Squidex/Config/Domain/BackupsServices.cs @@ -22,6 +22,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/Guards/GuardContentTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/Guards/GuardContentTests.cs index 33b085295..54eeaf444 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/Guards/GuardContentTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/Guards/GuardContentTests.cs @@ -34,6 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards private readonly ISchemaEntity normalUnpublishedSchema; private readonly ISchemaEntity singletonSchema; private readonly ISchemaEntity singletonUnpublishedSchema; + private readonly ISchemaEntity componentSchema; private readonly RefToken actor = RefToken.User("123"); public GuardContentTests() @@ -49,6 +50,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards singletonSchema = Mocks.Schema(appId, schemaId, new Schema(schemaId.Name, type: SchemaType.Singleton).Publish()); + + componentSchema = + Mocks.Schema(appId, schemaId, new Schema(schemaId.Name, type: SchemaType.Component).Publish()); } [Fact] @@ -91,6 +95,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards Assert.Throws(() => operation.MustNotCreateSingleton()); } + [Fact] + public void Should_throw_exception_if_creating_component_content() + { + var operation = Operation(CreateContent(Status.Draft), componentSchema); + + Assert.Throws(() => operation.MustNotCreateComponent()); + } + [Fact] public void Should_not_throw_exception_if_creating_singleton_content_with_schema_id() { diff --git a/backend/tests/Squidex.Infrastructure.Tests/Tasks/SchedulerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Tasks/SchedulerTests.cs index ebc35e0b9..2783c4b6d 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Tasks/SchedulerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Tasks/SchedulerTests.cs @@ -25,6 +25,21 @@ namespace Squidex.Infrastructure.Tasks Assert.Equal(new[] { 1 }, results.ToArray()); } + [Fact] + public async Task Should_schedule_lot_of_tasks_with_limited_concurrency() + { + var limited = new Scheduler(1); + + for (var i = 1; i <= 10; i++) + { + Schedule(i, limited); + } + + await limited.CompleteAsync(); + + Assert.Equal(Enumerable.Range(1, 10).ToArray(), results.OrderBy(x => x).ToArray()); + } + [Fact] public async Task Should_schedule_multiple_tasks() { @@ -83,5 +98,15 @@ namespace Squidex.Infrastructure.Tasks results.Add(value); }); } + + private void Schedule(int value, Scheduler target) + { + target.Schedule(async _ => + { + await Task.Delay(1); + + results.Add(value); + }); + } } } diff --git a/frontend/src/app/features/content/shared/list/content.component.html b/frontend/src/app/features/content/shared/list/content.component.html index 0bdf9826e..e217c136f 100644 --- a/frontend/src/app/features/content/shared/list/content.component.html +++ b/frontend/src/app/features/content/shared/list/content.component.html @@ -43,7 +43,7 @@ - x.canContentsCreate); if (this.schemaIds && this.schemaIds.length > 0) { - this.schemas = this.schemas.filter(x => this.schemaIds!.indexOf(x.id) >= 0); + this.schemas = this.schemas.filter(x => x.type === 'Default' && this.schemaIds!.indexOf(x.id) >= 0); } const selectedSchema = this.schemas.find(x => x.name === this.schemaName) || this.schemas[0]; diff --git a/frontend/src/app/features/schemas/pages/schema/fields/types/array-validation.component.ts b/frontend/src/app/features/schemas/pages/schema/fields/types/array-validation.component.ts index fd308febb..5efa4628e 100644 --- a/frontend/src/app/features/schemas/pages/schema/fields/types/array-validation.component.ts +++ b/frontend/src/app/features/schemas/pages/schema/fields/types/array-validation.component.ts @@ -7,7 +7,7 @@ import { Component, Input } from '@angular/core'; import { FormGroup } from '@angular/forms'; -import { ArrayFieldPropertiesDto, FieldDto, SchemaTagSource } from '@app/shared'; +import { ArrayFieldPropertiesDto, FieldDto } from '@app/shared'; @Component({ selector: 'sqx-array-validation[field][fieldForm][properties]', @@ -23,9 +23,4 @@ export class ArrayValidationComponent { @Input() public properties!: ArrayFieldPropertiesDto; - - constructor( - public readonly schemasSource: SchemaTagSource, - ) { - } } diff --git a/frontend/src/app/features/schemas/pages/schema/fields/types/references-validation.component.html b/frontend/src/app/features/schemas/pages/schema/fields/types/references-validation.component.html index 0d02c9640..92b989393 100644 --- a/frontend/src/app/features/schemas/pages/schema/fields/types/references-validation.component.html +++ b/frontend/src/app/features/schemas/pages/schema/fields/types/references-validation.component.html @@ -4,7 +4,7 @@
+ [converter]="(schemasSource.normalConverter | async)!" [suggestions]="(schemasSource.normalConverter | async)?.suggestions">
diff --git a/frontend/src/app/features/schemas/pages/schema/fields/types/string-ui.component.html b/frontend/src/app/features/schemas/pages/schema/fields/types/string-ui.component.html index 6a3470172..b9d367ccb 100644 --- a/frontend/src/app/features/schemas/pages/schema/fields/types/string-ui.component.html +++ b/frontend/src/app/features/schemas/pages/schema/fields/types/string-ui.component.html @@ -91,7 +91,7 @@
+ [converter]="(schemasSource.normalConverter | async)!" [suggestions]="(schemasSource.normalConverter | async)?.suggestions">
diff --git a/frontend/src/app/features/settings/pages/workflows/workflow.component.html b/frontend/src/app/features/settings/pages/workflows/workflow.component.html index 846509fc6..33ba797f3 100644 --- a/frontend/src/app/features/settings/pages/workflows/workflow.component.html +++ b/frontend/src/app/features/settings/pages/workflows/workflow.component.html @@ -4,7 +4,7 @@ {{workflow.displayName}}
- @@ -81,7 +81,7 @@ [disabled]="!isEditable" [ngModel]="workflow.schemaIds" (ngModelChange)="changeSchemaIds($event)" - [suggestions]="(schemasSource.converter | async)?.suggestions"> + [suggestions]="(schemasSource.normalConverter | async)?.suggestions"> diff --git a/frontend/src/app/shared/components/forms/rich-editor.component.ts b/frontend/src/app/shared/components/forms/rich-editor.component.ts index 20e379789..3f74d28a8 100644 --- a/frontend/src/app/shared/components/forms/rich-editor.component.ts +++ b/frontend/src/app/shared/components/forms/rich-editor.component.ts @@ -119,7 +119,7 @@ export class RichEditorComponent extends StatefulControlComponent<{}, string> im images_upload_handler: (blob: any, success: (url: string) => void, failure: (message: string) => void) => { const file = new File([blob.blob()], blob.filename(), { lastModified: new Date().getTime() }); - self.assetUploader.uploadFile(file) + self.assetUploader.uploadFile(file, undefined, this.folderId) .subscribe({ next: asset => { if (Types.is(asset, AssetDto)) { diff --git a/frontend/src/app/shared/components/references/content-selector.component.ts b/frontend/src/app/shared/components/references/content-selector.component.ts index 6b582ca81..033a3a423 100644 --- a/frontend/src/app/shared/components/references/content-selector.component.ts +++ b/frontend/src/app/shared/components/references/content-selector.component.ts @@ -81,7 +81,7 @@ export class ContentSelectorComponent extends ResourceOwner implements OnInit { this.schemas = this.schemasState.snapshot.schemas.filter(x => x.canReadContents); if (this.schemaIds && this.schemaIds.length > 0) { - this.schemas = this.schemas.filter(x => x.canReadContents && this.schemaIds!.includes(x.id)); + this.schemas = this.schemas.filter(x => x.type === 'Default' && x.canReadContents && this.schemaIds!.includes(x.id)); } this.selectSchema(this.schemas[0]); diff --git a/frontend/src/app/shared/state/schema-tag-source.ts b/frontend/src/app/shared/state/schema-tag-source.ts index 9b3ff1092..50a157578 100644 --- a/frontend/src/app/shared/state/schema-tag-source.ts +++ b/frontend/src/app/shared/state/schema-tag-source.ts @@ -16,7 +16,12 @@ class SchemaConverter implements TagConverter { constructor( private readonly schemas: ReadonlyArray, + normalOnly: boolean, ) { + if (normalOnly) { + schemas = schemas.filter(x => x.type === 'Default'); + } + this.suggestions = schemas.map(x => new TagValue(x.id, x.name, x.id)); } @@ -45,7 +50,11 @@ class SchemaConverter implements TagConverter { export class SchemaTagSource { public converter = this.schemasState.schemas.pipe( - map(x => new SchemaConverter(x))); + map(x => new SchemaConverter(x, false))); + + public normalConverter = + this.schemasState.schemas.pipe( + map(x => new SchemaConverter(x, true))); constructor( readonly schemasState: SchemasState,