From 64f411a89b8629c3cd79747a8acf3e4376ffb785 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sat, 19 Apr 2025 22:32:05 +0200 Subject: [PATCH] Use auto generated code (#1213) * Improve tests. Fix # Conflicts: # backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs # backend/src/Squidex.Data.MongoDb/ServiceExtensions.cs * Fix all tests * Add missing files. * More stuff. * Fix workflows stuff. * Fix state. * Temp * Build fix. * Fixes --- .../Api/Config/OpenApi/OpenApiServices.cs | 30 +- .../Api/Config/OpenApi/ScopesProcessor.cs | 3 +- .../Config/OpenApi/TagByGroupNameProcessor.cs | 1 - .../Api/Config/OpenApi/TagXmlProcessor.cs | 31 +- .../Contents/Generator/OperationBuilder.cs | 6 +- .../Generator/SchemasOpenApiGenerator.cs | 18 +- .../Contents/Models/BulkUpdateContentsDto.cs | 2 +- .../Models/EnrichContentDefaultsDto.cs | 2 +- .../Rules/Models/DynamicCreateRuleDto.cs | 27 + .../Rules/Models/DynamicRuleDto.cs | 81 + .../Rules/Models/DynamicRulesDto.cs | 24 + .../Rules/Models/DynamicUpdateRuleDto.cs | 34 + .../{CallsUsageDtoDto.cs => CallsUsageDto.cs} | 6 +- .../Statistics/UsagesController.cs | 8 +- backend/src/Squidex/Startup.cs | 1 - frontend/generator/Generator.sln | 22 + frontend/generator/Generator/Generator.csproj | 30 + frontend/generator/Generator/Program.cs | 165 + .../Generator/Templates/Class.liquid | 205 + .../Generator/Templates/ConvertToClass.liquid | 65 + .../Templates/ConvertToJavaScript.liquid | 45 + .../Templates/Enum.StringLiteral.liquid | 10 + .../event-consumer.component.ts | 4 +- .../event-consumers-page.component.ts | 4 +- .../pages/restore/restore-page.component.ts | 25 +- .../pages/users/user-page.component.ts | 51 +- .../services/event-consumers.service.spec.ts | 60 +- .../services/event-consumers.service.ts | 61 +- .../services/users.service.spec.ts | 100 +- .../administration/services/users.service.ts | 99 +- .../state/event-consumers.state.spec.ts | 8 +- .../state/event-consumers.state.ts | 4 +- .../administration/state/users.forms.ts | 7 +- .../administration/state/users.state.spec.ts | 32 +- .../administration/state/users.state.ts | 10 +- .../pages/asset-tag-dialog.component.ts | 1 - .../pages/calendar/calendar-page.component.ts | 4 +- .../pages/content/content-event.component.ts | 6 +- .../pages/content/content-page.component.ts | 5 +- .../pages/contents/contents-page.component.ts | 4 +- .../shared/due-time-selector.component.ts | 12 +- .../shared/forms/array-editor.component.ts | 2 +- .../shared/forms/array-item.component.ts | 6 +- .../shared/forms/content-field.component.ts | 8 +- .../shared/forms/content-section.component.ts | 4 +- .../shared/forms/field-editor.component.ts | 21 +- .../content/shared/list/content.component.ts | 27 +- .../references/content-creator.component.ts | 36 +- .../reference-dropdown.component.ts | 6 +- .../references/reference-item.component.ts | 6 +- .../references-checkboxes.component.ts | 4 +- .../references-radio-buttons.component.ts | 4 +- .../references/references-tag-converter.ts | 6 +- .../references/references-tags.component.ts | 6 +- .../rules/pages/rule/rule-page.component.html | 4 +- .../rules/pages/rule/rule-page.component.ts | 24 +- .../rules/pages/rules/rule.component.html | 6 +- .../rules/pages/rules/rule.component.ts | 14 +- .../rules/pages/rules/rules-page.component.ts | 8 +- .../actions/generic-action.component.html | 2 +- .../common/schema-edit-form.component.ts | 23 +- .../export/schema-export-form.component.ts | 23 +- .../schema/fields/field-group.component.ts | 8 +- .../schema/fields/field-wizard.component.html | 2 +- .../schema/fields/field-wizard.component.ts | 21 +- .../pages/schema/fields/field.component.ts | 35 +- .../forms/field-form-validation.component.ts | 4 +- .../fields/forms/field-form.component.ts | 4 +- .../fields/sortable-field-list.component.ts | 6 +- .../types/assets-validation.component.ts | 4 +- .../fields/types/boolean-ui.component.ts | 4 +- .../types/boolean-validation.component.ts | 4 +- .../fields/types/date-time-ui.component.ts | 4 +- .../types/date-time-validation.component.ts | 4 +- .../fields/types/number-ui.component.ts | 4 +- .../types/number-validation.component.ts | 6 +- .../fields/types/references-ui.component.ts | 4 +- .../types/references-validation.component.ts | 4 +- .../fields/types/string-ui.component.ts | 4 +- .../types/string-validation.component.ts | 8 +- .../schema/fields/types/tags-ui.component.ts | 4 +- .../fields/types/tags-validation.component.ts | 4 +- .../schema/indexes/index-form.component.ts | 25 +- .../schema-preview-urls-form.component.ts | 23 +- .../schema-field-rules-form.component.ts | 25 +- .../scripts/schema-scripts-form.component.ts | 23 +- .../pages/schema/ui/field-list.component.ts | 2 +- .../schema/ui/schema-ui-form.component.ts | 14 +- .../pages/schemas/schema-form.component.ts | 23 +- .../pages/schemas/schemas-page.component.ts | 13 +- .../asset-scripts-page.component.ts | 23 +- .../clients/client-add-form.component.ts | 23 +- .../pages/clients/client.component.html | 2 +- .../pages/clients/client.component.ts | 20 +- .../contributor-add-form.component.ts | 33 +- .../contributors/contributor.component.ts | 6 +- .../import-contributors-dialog.component.ts | 4 +- .../languages/language-add-form.component.ts | 23 +- .../pages/languages/language.component.ts | 31 +- .../pages/more/more-page.component.ts | 54 +- .../settings/pages/plans/plan.component.ts | 2 +- .../pages/roles/role-add-form.component.ts | 23 +- .../settings/pages/roles/role.component.ts | 27 +- .../settings/settings-page.component.html | 4 +- .../pages/settings/settings-page.component.ts | 23 +- .../workflows/workflow-add-form.component.ts | 23 +- .../workflows/workflow-diagram.component.ts | 32 +- .../workflows/workflow-step.component.html | 22 +- .../workflows/workflow-step.component.ts | 20 +- .../workflow-transition.component.html | 8 +- .../pages/workflows/workflow.component.html | 26 +- .../pages/workflows/workflow.component.ts | 52 +- .../teams/pages/auth/auth-page.component.ts | 26 +- .../contributor-add-form.component.ts | 33 +- .../contributors/contributor.component.ts | 6 +- .../import-contributors-dialog.component.ts | 4 +- .../teams/pages/more/more-page.component.ts | 23 +- .../teams/pages/plans/plan.component.ts | 2 +- .../team-contributors.service.spec.ts | 54 +- .../services/team-contributors.service.ts | 16 +- .../teams/services/team-plans.service.spec.ts | 74 +- .../teams/services/team-plans.service.ts | 12 +- .../features/teams/state/team-auth.forms.ts | 2 +- .../teams/state/team-auth.state.spec.ts | 33 +- .../features/teams/state/team-auth.state.ts | 16 +- .../teams/state/team-contributors.forms.ts | 12 +- .../state/team-contributors.state.spec.ts | 10 +- .../teams/state/team-contributors.state.ts | 10 +- .../teams/state/team-plans.state.spec.ts | 48 +- .../features/teams/state/team-plans.state.ts | 16 +- .../angular/forms/editable-title.component.ts | 2 +- .../framework/angular/http/http-extensions.ts | 26 +- .../angular/long-hover.directive.stories.ts | 8 +- .../angular/markdown.directive.spec.ts | 4 +- .../framework/angular/markdown.directive.ts | 29 +- .../framework/angular/pipes/keys.pipe.spec.ts | 12 + .../app/framework/angular/pipes/keys.pipe.ts | 2 +- .../framework/services/localizer.service.ts | 1 - .../app/framework/services/title.service.ts | 29 +- frontend/src/app/framework/utils/hateos.ts | 4 +- .../app/framework/utils/rxjs-extensions.ts | 4 +- .../src/app/framework/utils/version.spec.ts | 12 +- frontend/src/app/framework/utils/version.ts | 23 +- .../shared/components/app-form.component.ts | 27 +- .../assets/asset-dialog.component.ts | 4 +- .../assets/image-focus-point.component.ts | 10 +- .../src/app/shared/components/assets/pipes.ts | 6 +- .../contents/content-list-field.component.ts | 6 +- .../contents/content-list-header.component.ts | 4 +- .../contents/content-status.component.ts | 4 +- .../contents/translation-status.component.ts | 6 +- .../components/forms/rich-editor.component.ts | 8 +- .../content-selector-item.component.ts | 6 +- .../references/content-selector.component.ts | 6 +- .../references/reference-input.component.ts | 6 +- .../queries/filter-comparison.component.ts | 6 +- .../queries/filter-logical.component.ts | 6 +- .../search/queries/filter-node.component.ts | 6 +- .../search/queries/query.component.ts | 6 +- .../search/search-form.component.ts | 27 +- .../components/table-header.component.ts | 4 +- .../shared/components/team-form.component.ts | 23 +- .../guards/rule-must-exist.guard.spec.ts | 4 +- frontend/src/app/shared/internal.ts | 3 +- frontend/src/app/shared/model/custom.ts | 962 + frontend/src/app/shared/model/generated.ts | 14839 ++++++++++++++++ frontend/src/app/shared/model/index.ts | 9 + frontend/src/app/shared/model/schemas.ts | 287 + .../services/app-languages.service.spec.ts | 57 +- .../shared/services/app-languages.service.ts | 87 +- .../app/shared/services/apps.service.spec.ts | 155 +- .../src/app/shared/services/apps.service.ts | 234 +- .../shared/services/assets.service.spec.ts | 146 +- .../src/app/shared/services/assets.service.ts | 256 +- .../shared/services/autosave.service.spec.ts | 16 +- .../app/shared/services/autosave.service.ts | 2 +- .../shared/services/clients.service.spec.ts | 58 +- .../app/shared/services/clients.service.ts | 96 +- .../shared/services/contents.service.spec.ts | 135 +- .../app/shared/services/contents.service.ts | 240 +- .../services/contributors.service.spec.ts | 52 +- .../shared/services/contributors.service.ts | 20 +- .../shared/services/history.service.spec.ts | 48 +- .../app/shared/services/history.service.ts | 35 +- .../shared/services/indexes.service.spec.ts | 33 +- .../app/shared/services/indexes.service.ts | 52 +- .../app/shared/services/jobs.service.spec.ts | 99 +- .../src/app/shared/services/jobs.service.ts | 114 +- .../shared/services/languages.service.spec.ts | 15 +- .../app/shared/services/languages.service.ts | 21 +- .../app/shared/services/news.service.spec.ts | 39 +- .../src/app/shared/services/news.service.ts | 24 +- .../app/shared/services/plans.service.spec.ts | 74 +- .../src/app/shared/services/plans.service.ts | 16 +- .../app/shared/services/roles.service.spec.ts | 63 +- .../src/app/shared/services/roles.service.ts | 86 +- .../app/shared/services/rules.service.spec.ts | 261 +- .../src/app/shared/services/rules.service.ts | 331 +- .../shared/services/schemas.service.spec.ts | 299 +- .../app/shared/services/schemas.service.ts | 674 +- .../src/app/shared/services/schemas.spec.ts | 14 +- .../src/app/shared/services/schemas.types.ts | 549 - .../shared/services/search.service.spec.ts | 19 +- .../src/app/shared/services/search.service.ts | 34 +- frontend/src/app/shared/services/shared.ts | 159 - .../app/shared/services/teams.service.spec.ts | 85 +- .../src/app/shared/services/teams.service.ts | 125 +- .../shared/services/templates.service.spec.ts | 42 +- .../app/shared/services/templates.service.ts | 55 +- .../services/translations.service.spec.ts | 15 +- .../shared/services/translations.service.ts | 37 +- .../shared/services/usages.service.spec.ts | 107 +- .../src/app/shared/services/usages.service.ts | 97 +- .../services/users-provider.service.spec.ts | 10 +- .../shared/services/users-provider.service.ts | 7 +- .../app/shared/services/users.service.spec.ts | 18 +- .../src/app/shared/services/users.service.ts | 35 +- .../shared/services/workflows.service.spec.ts | 397 +- .../app/shared/services/workflows.service.ts | 298 +- .../src/app/shared/state/_test-helpers.ts | 79 +- frontend/src/app/shared/state/apps.forms.ts | 26 +- .../src/app/shared/state/apps.state.spec.ts | 8 +- frontend/src/app/shared/state/apps.state.ts | 7 +- .../shared/state/asset-scripts.state.spec.ts | 21 +- .../app/shared/state/asset-scripts.state.ts | 21 +- .../app/shared/state/asset-uploader.state.ts | 3 +- .../src/app/shared/state/assets.forms.spec.ts | 2 +- frontend/src/app/shared/state/assets.forms.ts | 80 +- .../src/app/shared/state/assets.state.spec.ts | 76 +- frontend/src/app/shared/state/assets.state.ts | 19 +- .../src/app/shared/state/backups.forms.ts | 8 +- .../src/app/shared/state/clients.forms.ts | 10 +- .../app/shared/state/clients.state.spec.ts | 10 +- .../src/app/shared/state/clients.state.ts | 10 +- .../app/shared/state/contents.form-rules.ts | 4 +- .../shared/state/contents.forms-helpers.ts | 19 +- .../app/shared/state/contents.forms.spec.ts | 81 +- .../src/app/shared/state/contents.forms.ts | 75 +- .../shared/state/contents.forms.visitors.ts | 31 +- .../src/app/shared/state/contents.state.ts | 43 +- .../app/shared/state/contributors.forms.ts | 12 +- .../shared/state/contributors.state.spec.ts | 10 +- .../app/shared/state/contributors.state.ts | 11 +- .../src/app/shared/state/indexes.forms.ts | 8 +- .../app/shared/state/indexes.state.spec.ts | 4 +- .../src/app/shared/state/indexes.state.ts | 3 +- frontend/src/app/shared/state/jobs.state.ts | 3 +- .../src/app/shared/state/languages.forms.ts | 16 +- .../app/shared/state/languages.state.spec.ts | 14 +- .../src/app/shared/state/languages.state.ts | 20 +- .../src/app/shared/state/plans.state.spec.ts | 48 +- frontend/src/app/shared/state/plans.state.ts | 19 +- frontend/src/app/shared/state/resolvers.ts | 4 +- frontend/src/app/shared/state/roles.forms.ts | 16 +- .../src/app/shared/state/roles.state.spec.ts | 8 +- frontend/src/app/shared/state/roles.state.ts | 13 +- .../shared/state/rule-events.state.spec.ts | 36 +- .../src/app/shared/state/rule-events.state.ts | 26 +- .../shared/state/rule-simulator.state.spec.ts | 22 +- .../app/shared/state/rule-simulator.state.ts | 3 +- frontend/src/app/shared/state/rules.forms.ts | 12 +- .../src/app/shared/state/rules.state.spec.ts | 37 +- frontend/src/app/shared/state/rules.state.ts | 25 +- .../src/app/shared/state/schema-tag-source.ts | 2 +- .../src/app/shared/state/schemas.forms.ts | 29 +- .../app/shared/state/schemas.state.spec.ts | 195 +- .../src/app/shared/state/schemas.state.ts | 37 +- .../app/shared/state/table-settings.spec.ts | 28 +- .../src/app/shared/state/table-settings.ts | 2 +- frontend/src/app/shared/state/teams.forms.ts | 10 +- .../src/app/shared/state/teams.state.spec.ts | 12 +- .../app/shared/state/template.state.spec.ts | 6 +- .../src/app/shared/state/templates.state.ts | 3 +- .../src/app/shared/state/ui.state.spec.ts | 14 +- .../src/app/shared/state/workflows.forms.ts | 8 +- .../app/shared/state/workflows.state.spec.ts | 348 +- .../src/app/shared/state/workflows.state.ts | 218 +- .../src/app/shared/utils/editor-utils.spec.ts | 35 +- frontend/src/app/shared/utils/editor-utils.ts | 3 +- frontend/src/spec/matchers.ts | 34 + frontend/tsconfig.spec.json | 3 +- frontend/types/matchers.d.ts | 12 + 282 files changed, 20767 insertions(+), 6324 deletions(-) create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Rules/Models/DynamicCreateRuleDto.cs create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Rules/Models/DynamicRuleDto.cs create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Rules/Models/DynamicRulesDto.cs create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Rules/Models/DynamicUpdateRuleDto.cs rename backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/{CallsUsageDtoDto.cs => CallsUsageDto.cs} (91%) create mode 100644 frontend/generator/Generator.sln create mode 100644 frontend/generator/Generator/Generator.csproj create mode 100644 frontend/generator/Generator/Program.cs create mode 100644 frontend/generator/Generator/Templates/Class.liquid create mode 100644 frontend/generator/Generator/Templates/ConvertToClass.liquid create mode 100644 frontend/generator/Generator/Templates/ConvertToJavaScript.liquid create mode 100644 frontend/generator/Generator/Templates/Enum.StringLiteral.liquid create mode 100644 frontend/src/app/shared/model/custom.ts create mode 100644 frontend/src/app/shared/model/generated.ts create mode 100644 frontend/src/app/shared/model/index.ts create mode 100644 frontend/src/app/shared/model/schemas.ts delete mode 100644 frontend/src/app/shared/services/schemas.types.ts delete mode 100644 frontend/src/app/shared/services/shared.ts create mode 100644 frontend/src/spec/matchers.ts create mode 100644 frontend/types/matchers.d.ts diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs index 0b043d833..478a6f370 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs @@ -12,6 +12,8 @@ using NJsonSchema.Generation.TypeMappers; using NodaTime; using NSwag.Generation; using NSwag.Generation.Processors; +using NSwag.Generation.Processors.Contexts; +using Squidex.Areas.Api.Controllers.Rules.Models; using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; @@ -88,6 +90,9 @@ public static class OpenApiServices services.AddOpenApiDocument((settings, services) => { ConfigureSchemaSettings(settings.SchemaSettings, services.GetRequiredService(), false); + settings.DocumentProcessors.Add(new AddAdditionalTypeProcessor()); + settings.DocumentProcessors.Add(new AddAdditionalTypeProcessor()); + settings.DocumentProcessors.Add(new AddAdditionalTypeProcessor()); }); } @@ -103,8 +108,8 @@ public static class OpenApiServices settings.SchemaProcessors.Add(new RequiredSchemaProcessor()); settings.SchemaType = NJsonSchema.SchemaType.OpenApi3; - settings.TypeMappers = new List - { + settings.TypeMappers = + [ CreateAnyMap>(), CreateAnyMap(), CreateAnyMap(), @@ -123,15 +128,14 @@ public static class OpenApiServices CreateStringMap(), CreateStringMap(), CreateStringMap(), - }; + ]; } - private static ITypeMapper CreateObjectMap() + private static PrimitiveTypeMapper CreateObjectMap() { return new PrimitiveTypeMapper(typeof(T), schema => { schema.Type = JsonObjectType.Object; - schema.AdditionalPropertiesSchema = new JsonSchema { Description = "Any", @@ -139,12 +143,11 @@ public static class OpenApiServices }); } - private static ITypeMapper CreateArrayMap(JsonObjectType itemType) + private static PrimitiveTypeMapper CreateArrayMap(JsonObjectType itemType) { return new PrimitiveTypeMapper(typeof(T), schema => { schema.Type = JsonObjectType.Array; - schema.Item = new JsonSchema { Type = itemType, @@ -152,21 +155,28 @@ public static class OpenApiServices }); } - private static ITypeMapper CreateStringMap(string? format = null) + private static PrimitiveTypeMapper CreateStringMap(string? format = null) { return new PrimitiveTypeMapper(typeof(T), schema => { schema.Type = JsonObjectType.String; - schema.Format = format; }); } - private static ITypeMapper CreateAnyMap() + private static PrimitiveTypeMapper CreateAnyMap() { return new PrimitiveTypeMapper(typeof(T), schema => { schema.Type = JsonObjectType.None; }); } + + public sealed class AddAdditionalTypeProcessor : IDocumentProcessor where T : class + { + public void Process(DocumentProcessorContext context) + { + context.SchemaResolver.AppendSchema(context.SchemaGenerator.Generate(typeof(T), context.SchemaResolver), null); + } + } } diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/ScopesProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/ScopesProcessor.cs index 4618fc902..3dfcd3d6a 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/ScopesProcessor.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/ScopesProcessor.cs @@ -18,10 +18,9 @@ public sealed class ScopesProcessor : IOperationProcessor { public bool Process(OperationProcessorContext context) { - context.OperationDescription.Operation.Security ??= new List(); + context.OperationDescription.Operation.Security ??= []; var permissionAttribute = context.MethodInfo.GetCustomAttribute(); - if (permissionAttribute != null) { context.OperationDescription.Operation.Security.Add(new OpenApiSecurityRequirement diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/TagByGroupNameProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/TagByGroupNameProcessor.cs index fa32f369e..ed84b607b 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/TagByGroupNameProcessor.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/TagByGroupNameProcessor.cs @@ -21,7 +21,6 @@ public sealed class TagByGroupNameProcessor : IOperationProcessor if (!string.IsNullOrWhiteSpace(groupName)) { context.OperationDescription.Operation.Tags = [groupName]; - return true; } else diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/TagXmlProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/TagXmlProcessor.cs index 1c281d8fa..df77c237e 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/TagXmlProcessor.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/TagXmlProcessor.cs @@ -22,25 +22,28 @@ public sealed class TagXmlProcessor : IDocumentProcessor foreach (var controllerType in context.ControllerTypes) { var attribute = controllerType.GetCustomAttribute(); + if (attribute == null) + { + continue; + } - if (attribute != null) + var tag = context.Document.Tags.FirstOrDefault(x => x.Name == attribute.GroupName); + if (tag == null) { - var tag = context.Document.Tags.FirstOrDefault(x => x.Name == attribute.GroupName); + continue; + } - if (tag != null) - { - var description = controllerType.GetXmlDocsSummary(); + var description = controllerType.GetXmlDocsSummary(); + if (description == null) + { + continue; + } - if (description != null) - { - tag.Description ??= string.Empty; + tag.Description ??= string.Empty; - if (!tag.Description.Contains(description, StringComparison.Ordinal)) - { - tag.Description += "\n\n" + description; - } - } - } + if (!tag.Description.Contains(description, StringComparison.Ordinal)) + { + tag.Description += "\n\n" + description; } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationBuilder.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationBuilder.cs index 8bbd053f7..ae6fa0392 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationBuilder.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationBuilder.cs @@ -138,13 +138,13 @@ internal sealed class OperationBuilder(OperationsBuilder operations, OpenApiOper { var fullId = PermissionIds.ForApp(permissionId, operations.Parent.AppName, operations.SchemaName).Id; - operation.Security = new List - { + operation.Security = + [ new OpenApiSecurityRequirement { [Constants.SecurityDefinition] = [fullId], }, - }; + ]; return this; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs index 2636a488d..0748f892f 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs @@ -221,18 +221,18 @@ public sealed class SchemasOpenApiGenerator( var document = new OpenApiDocument { - Schemes = new List - { + Schemes = + [ scheme, - }, - Consumes = new List - { + ], + Consumes = + [ "application/json", - }, - Produces = new List - { + ], + Produces = + [ "application/json", - }, + ], Info = new OpenApiInfo { Title = $"Squidex Content API for '{appName}' App", diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsDto.cs index 6cf20c48d..3aafe3123 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsDto.cs @@ -34,7 +34,7 @@ public sealed class BulkUpdateContentsDto public bool DoNotScript { get; set; } = true; /// - /// True, to also enrich required fields. Default: false. + /// True, to also enrich required fields. Default: false. /// public bool EnrichRequiredFields { get; set; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/EnrichContentDefaultsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/EnrichContentDefaultsDto.cs index cb72f5caf..fcff71d71 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/EnrichContentDefaultsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/EnrichContentDefaultsDto.cs @@ -15,7 +15,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models; public class EnrichContentDefaultsDto { /// - /// True, to also enrich required fields. Default: false. + /// True, to also enrich required fields. Default: false. /// [FromQuery(Name = "enrichRequiredFields")] public bool EnrichRequiredFields { get; set; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/DynamicCreateRuleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/DynamicCreateRuleDto.cs new file mode 100644 index 000000000..ff97060e4 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/DynamicCreateRuleDto.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure.Validation; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Rules.Models; + +[OpenApiRequest] +public sealed class DynamicCreateRuleDto +{ + /// + /// The trigger properties. + /// + [LocalizedRequired] + public RuleTriggerDto Trigger { get; set; } + + /// + /// The action properties. + /// + [LocalizedRequired] + public Dictionary Action { get; set; } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/DynamicRuleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/DynamicRuleDto.cs new file mode 100644 index 000000000..147a7dcc7 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/DynamicRuleDto.cs @@ -0,0 +1,81 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NodaTime; +using Squidex.Infrastructure; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Rules.Models; + +public sealed class DynamicRuleDto : Resource +{ + /// + /// The ID of the rule. + /// + public DomainId Id { get; set; } + + /// + /// The user that has created the rule. + /// + public RefToken CreatedBy { get; set; } + + /// + /// The user that has updated the rule. + /// + public RefToken LastModifiedBy { get; set; } + + /// + /// The date and time when the rule has been created. + /// + public Instant Created { get; set; } + + /// + /// The date and time when the rule has been modified last. + /// + public Instant LastModified { get; set; } + + /// + /// The version of the rule. + /// + public long Version { get; set; } + + /// + /// Determines if the rule is enabled. + /// + public bool IsEnabled { get; set; } + + /// + /// Optional rule name. + /// + public string? Name { get; set; } + + /// + /// The trigger properties. + /// + public RuleTriggerDto Trigger { get; set; } + + /// + /// The action properties. + /// + public Dictionary Action { get; set; } + + /// + /// The number of completed executions. + /// + public long NumSucceeded { get; set; } + + /// + /// The number of failed executions. + /// + public long NumFailed { get; set; } + + /// + /// The date and time when the rule was executed the last time. + /// + [Obsolete("Removed when migrated to new rule statistics.")] + public Instant? LastExecuted { get; set; } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/DynamicRulesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/DynamicRulesDto.cs new file mode 100644 index 000000000..4fa938797 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/DynamicRulesDto.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Rules.Models; + +public sealed class DynamicRulesDto : Resource +{ + /// + /// The rules. + /// + public DynamicRuleDto[] Items { get; set; } + + /// + /// The ID of the rule that is currently rerunning. + /// + public DomainId? RunningRuleId { get; set; } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/DynamicUpdateRuleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/DynamicUpdateRuleDto.cs new file mode 100644 index 000000000..60263f3a3 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/DynamicUpdateRuleDto.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Rules.Models; + +[OpenApiRequest] +public sealed class DynamicUpdateRuleDto +{ + /// + /// Optional rule name. + /// + public string? Name { get; set; } + + /// + /// The trigger properties. + /// + public RuleTriggerDto? Trigger { get; set; } + + /// + /// The action properties. + /// + public Dictionary? Action { get; set; } + + /// + /// Enable or disable the rule. + /// + public bool? IsEnabled { get; set; } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsageDtoDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsageDto.cs similarity index 91% rename from backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsageDtoDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsageDto.cs index 67b48bcc3..49e3f1289 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsageDtoDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsageDto.cs @@ -10,7 +10,7 @@ using Squidex.Infrastructure.UsageTracking; namespace Squidex.Areas.Api.Controllers.Statistics.Models; -public sealed class CallsUsageDtoDto +public sealed class CallsUsageDto { /// /// The total number of API calls. @@ -57,9 +57,9 @@ public sealed class CallsUsageDtoDto /// public Dictionary Details { get; set; } - public static CallsUsageDtoDto FromDomain(Plan plan, ApiStatsSummary summary, Dictionary> details) + public static CallsUsageDto FromDomain(Plan plan, ApiStatsSummary summary, Dictionary> details) { - return new CallsUsageDtoDto + return new CallsUsageDto { AverageElapsedMs = summary.AverageElapsedMs, BlockingApiCalls = plan.BlockingApiCalls, diff --git a/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs index 4e5f5fe33..b7d9b29e2 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs @@ -93,7 +93,7 @@ public sealed class UsagesController( /// App not found. [HttpGet] [Route("apps/{app}/usages/calls/{fromDate}/{toDate}/")] - [ProducesResponseType(typeof(CallsUsageDtoDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(CallsUsageDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(PermissionIds.AppUsage)] [ApiCosts(0)] public async Task GetUsages(string app, DateOnly fromDate, DateOnly toDate) @@ -103,7 +103,7 @@ public sealed class UsagesController( // Use the current app plan to show the limits to the user. var (plan, _, _) = await usageGate.GetPlanForAppAsync(App, false, HttpContext.RequestAborted); - var response = CallsUsageDtoDto.FromDomain(plan, summary, details); + var response = CallsUsageDto.FromDomain(plan, summary, details); return Ok(response); } @@ -118,7 +118,7 @@ public sealed class UsagesController( /// Team not found. [HttpGet] [Route("teams/{team}/usages/calls/{fromDate}/{toDate}/")] - [ProducesResponseType(typeof(CallsUsageDtoDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(CallsUsageDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(PermissionIds.TeamUsage)] [ApiCosts(0)] public async Task GetUsagesForTeam(string team, DateOnly fromDate, DateOnly toDate) @@ -128,7 +128,7 @@ public sealed class UsagesController( // Use the current team plan to show the limits to the user. var (plan, _) = await usageGate.GetPlanForTeamAsync(Team, HttpContext.RequestAborted); - var response = CallsUsageDtoDto.FromDomain(plan, summary, details); + var response = CallsUsageDto.FromDomain(plan, summary, details); return Ok(response); } diff --git a/backend/src/Squidex/Startup.cs b/backend/src/Squidex/Startup.cs index 44aeb1e68..27c237362 100644 --- a/backend/src/Squidex/Startup.cs +++ b/backend/src/Squidex/Startup.cs @@ -71,7 +71,6 @@ public sealed class Startup(IConfiguration config) public void Configure(IApplicationBuilder app) { - app.UseWebSockets(); app.UseCookiePolicy(); app.UseDefaultPathBase(); diff --git a/frontend/generator/Generator.sln b/frontend/generator/Generator.sln new file mode 100644 index 000000000..9b3263c02 --- /dev/null +++ b/frontend/generator/Generator.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.12.35707.178 d17.12 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Generator", "Generator\Generator.csproj", "{0BD7AC0B-640B-4D26-A9B9-C1CCEE4C14E7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0BD7AC0B-640B-4D26-A9B9-C1CCEE4C14E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0BD7AC0B-640B-4D26-A9B9-C1CCEE4C14E7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0BD7AC0B-640B-4D26-A9B9-C1CCEE4C14E7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0BD7AC0B-640B-4D26-A9B9-C1CCEE4C14E7}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/frontend/generator/Generator/Generator.csproj b/frontend/generator/Generator/Generator.csproj new file mode 100644 index 000000000..18d724789 --- /dev/null +++ b/frontend/generator/Generator/Generator.csproj @@ -0,0 +1,30 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + Always + + + Always + + + Always + + + Always + + + + diff --git a/frontend/generator/Generator/Program.cs b/frontend/generator/Generator/Program.cs new file mode 100644 index 000000000..becdbf0d9 --- /dev/null +++ b/frontend/generator/Generator/Program.cs @@ -0,0 +1,165 @@ +using NJsonSchema; +using NJsonSchema.CodeGeneration.TypeScript; +using NSwag; +using NSwag.CodeGeneration.TypeScript; +using System.Text.RegularExpressions; + +namespace Generator; + +internal partial class Program +{ + static async Task Main(string[] args) + { + var cacheFile = "cache.json"; + + if (!File.Exists(cacheFile) || !args.Contains("--cache")) + { + var httpClient = new HttpClient(); + var schemaResponse = await httpClient.GetAsync("https://localhost:5001/api/swagger/v1/swagger.json"); + var schemaText = await schemaResponse.Content.ReadAsStringAsync(); + + File.WriteAllText(cacheFile, schemaText); + } + + var codePath = GetCodePath(); + + var document = await OpenApiDocument.FromJsonAsync(File.ReadAllText(cacheFile)); + + foreach (var (typeName, schema) in document.Components.Schemas.ToList()) + { + if (typeName.Equals("AssetDto")) + { + if (schema.ActualProperties.TryGetValue("tags", out var tags)) + { + tags.IsNullableRaw = false; + tags.IsRequired = true; + } + } + + if (typeName.Equals("LanguageDto")) + { + schema.Properties.Remove("nativeName"); + } + + if (typeName.Equals("AppDto") || typeName.Equals("TeamDto")) + { + if (schema.ActualProperties.ContainsKey("created") && !schema.ActualProperties.ContainsKey("createdBy")) + { + schema.Properties["createdBy"] = new JsonSchemaProperty + { + Description = "The user that has created the app.", + Type = JsonObjectType.String, + IsNullableRaw = true, + IsRequired = false, + IsReadOnly = true, + }; + } + + if (schema.ActualProperties.ContainsKey("lastModified") && !schema.ActualProperties.ContainsKey("lastModifiedBy")) + { + schema.Properties["lastModifiedBy"] = new JsonSchemaProperty + { + Description = "The user that has updated the app.", + Type = JsonObjectType.String, + IsNullableRaw = true, + IsRequired = false, + IsReadOnly = true, + }; + } + } + + foreach (var (name, property) in schema.ActualProperties) + { + property.IsReadOnly = true; + + if (property.Type == JsonObjectType.String && !property.IsRequired) + { + property.IsNullableRaw = true; + } + + if (name.Equals("referenceFields")) + { + property.IsNullableRaw = false; + property.IsRequired = true; + } + + if (name.Equals("schemaName")) + { + property.IsNullableRaw = false; + property.IsRequired = true; + } + + if (name.Equals("schemaDisplayName")) + { + property.IsNullableRaw = false; + property.IsRequired = true; + } + } + + if (document.Components.Schemas.TryGetValue("ErrorDto", out var error)) + { + document.Components.Schemas.Remove("ErrorDto"); + document.Components.Schemas["ServerErrorDto"] = error; + } + + if (!typeName.EndsWith("Dto") && schema.Type == JsonObjectType.Object) + { + document.Components.Schemas.Remove(typeName); + document.Components.Schemas[$"{typeName}Dto"] = schema; + } + } + + var extensionFile = Path.Combine(codePath, @"..\\..\\src\\app\\shared\\model\\custom.ts"); + var extensionCode = File.ReadAllText(extensionFile); + + var classes = + ClassNameRegex().Matches(extensionCode) + .Select(m => m.Groups["ClassName"].Value) + .ToArray(); + + var settings = new TypeScriptClientGeneratorSettings + { + GenerateClientClasses = false, + GenerateClientInterfaces = false, + }; + settings.TypeScriptGeneratorSettings.EnumStyle = TypeScriptEnumStyle.StringLiteral; + settings.TypeScriptGeneratorSettings.ExportTypes = true; + settings.TypeScriptGeneratorSettings.ExtendedClasses = classes; + settings.TypeScriptGeneratorSettings.ExtensionCode = extensionCode; + settings.TypeScriptGeneratorSettings.GenerateConstructorInterface = true; + settings.TypeScriptGeneratorSettings.InlineNamedDictionaries = true; + settings.TypeScriptGeneratorSettings.TemplateDirectory = Path.Combine(Directory.GetCurrentDirectory(), "Templates"); + + var generator = new TypeScriptClientGenerator(document, settings); + + var code = generator.GenerateFile(); + + code = code.Replace("I{ [key: string]", "{ [key: string]"); + code = code.Replace(": Date,", ": DateTime,"); + code = code.Replace(": Date;", ": DateTime;"); + code = code.Replace(": Date |", ": DateTime |"); + code = code.Replace("DtoDto", "Dto"); + + var targetFolder = Path.Combine(codePath, @"..\\..\\src\\app\\shared\\model\\generated.ts"); + + File.WriteAllText(targetFolder, code); + } + + [GeneratedRegex("class (?[^\\)]*) extends generated\\.")] + private static partial Regex ClassNameRegex(); + + private static string GetCodePath() + { + var folder = new DirectoryInfo(Directory.GetCurrentDirectory()); + + while (true) + { + if (folder.Name.Equals("Generator")) + { + return folder.FullName; + } + + folder = folder.Parent!; + } + } +} \ No newline at end of file diff --git a/frontend/generator/Generator/Templates/Class.liquid b/frontend/generator/Generator/Templates/Class.liquid new file mode 100644 index 000000000..49644c24b --- /dev/null +++ b/frontend/generator/Generator/Templates/Class.liquid @@ -0,0 +1,205 @@ +{% if HasDescription -%} +/** {{ Description }} */ +{% endif -%} +{% if ExportTypes %}export {% endif %}{% if IsAbstract %}abstract {% endif %}class {{ ClassName }}{{ Inheritance }} { +{% if HasDiscriminator -%} + /** The discriminator. */ + public readonly {{ BaseDiscriminator }}!: string; +{% endif -%} +{% unless HasInheritance -%} + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; +{% endunless -%} +{% for property in Properties -%} +{% if property.HasDescription -%} + /** {{ property.Description | strip }} */ +{% endif -%} + {% if property.IsReadOnly %}readonly {% endif %}{{ property.PropertyName }}{% if property.IsOptional %}?{% elsif RequiresStrictPropertyInitialization %}!{% endif %}: {{ property.Type }}{{ property.TypePostfix }}; +{% endfor -%} +{% if HasIndexerProperty -%} + + [key: string]: {{ IndexerPropertyValueType }}; +{% endif -%} + +{% assign condition_temp = HasInheritance == false or ConvertConstructorInterfaceData -%} +{% if GenerateConstructorInterface or HasBaseDiscriminator -%} + constructor({% if GenerateConstructorInterface %}data?: I{{ ClassName }}{% endif %}) { +{% if HasInheritance -%} + super({% if GenerateConstructorInterface %}data{% endif %}); +{% endif -%} +{% if GenerateConstructorInterface and condition_temp -%} + if (data) { +{% if HasInheritance == false -%} + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } +{% endif -%} +{% if ConvertConstructorInterfaceData -%} +{% for property in Properties -%} +{% if property.SupportsConstructorConversion -%} +{% if property.IsArray -%} + if (data.{{ property.PropertyName }}) { + this.{{ property.PropertyName }} = []; + for (let i = 0; i < data.{{ property.PropertyName }}.length; i++) { + let item = data.{{ property.PropertyName }}[i]; + this.{{ property.PropertyName }}[i] = item && !(item).toJSON ? new {{ property.ArrayItemType }}(item) : <{{ property.ArrayItemType }}>item; + } + } +{% elsif property.IsDictionary -%} + if (data.{{ property.PropertyName }}) { + this.{{ property.PropertyName }} = {}; + for (let key in data.{{ property.PropertyName }}) { + if (data.{{ property.PropertyName }}.hasOwnProperty(key)) { + let item = data.{{ property.PropertyName }}[key]; + this.{{ property.PropertyName }}[key] = item && !(item).toJSON ? new {{ property.DictionaryItemType }}(item) : <{{ property.DictionaryItemType }}>item; + } + } + } +{% else -%} + this.{{ property.PropertyName }} = data.{{ property.PropertyName }} && !(data.{{ property.PropertyName }}).toJSON ? new {{ property.Type }}(data.{{ property.PropertyName }}) : <{{ property.Type }}>this.{{ property.PropertyName }}; +{% endif -%} +{% endif -%} +{% endfor -%} +{% endif -%} + } +{% endif -%} +{% if HasBaseDiscriminator -%} + (this).{{ BaseDiscriminator }} = "{{ DiscriminatorName }}"; +{% endif -%} + } +{% endif -%} + + {% if HasInheritance and SupportsOverrideKeyword %}override {% endif %}init(_data: any{% if HandleReferences %}, _mappings?: any{% endif %}) { +{% if HasInheritance -%} + super.init(_data); +{% endif -%} +{% if HasIndexerProperty or HasProperties -%} +{% if HasIndexerProperty -%} + for (var property in _data) { + if (_data.hasOwnProperty(property)) + this[property] = _data[property]; + } +{% endif -%} +{% for property in Properties -%} + {{ property.ConvertToClassCode | strip | tab }} +{% endfor -%} +{% endif -%} + this.cleanup(this); + return this; + } + + static {% if HasInheritance and SupportsOverrideKeyword %}override {% endif %}fromJSON(data: any{% if HandleReferences %}, _mappings?: any{% endif %}): {{ ClassName }} { +{% if HandleReferences -%} +{% if HasBaseDiscriminator -%} +{% for derivedClass in DerivedClasses -%} + if (data["{{ BaseDiscriminator }}"] === "{{ derivedClass.Discriminator }}") +{% if derivedClass.IsAbstract -%} + throw new Error("The abstract class '{{ derivedClass.ClassName }}' cannot be instantiated."); +{% else -%} + return createInstance<{{ derivedClass.ClassName }}>(data, _mappings, {{ derivedClass.ClassName }}); +{% endif -%} +{% endfor -%} +{% endif -%} +{% if IsAbstract -%} + throw new Error("The abstract class '{{ ClassName }}' cannot be instantiated."); +{% else -%} + return createInstance<{{ ClassName }}>(data, _mappings, {{ ClassName }}); +{% endif -%} +{% else -%} +{% if HasBaseDiscriminator -%} +{% for derivedClass in DerivedClasses -%} + if (data["{{ BaseDiscriminator }}"] === "{{ derivedClass.Discriminator }}") { +{% if derivedClass.IsAbstract -%} + throw new Error("The abstract class '{{ derivedClass.ClassName }}' cannot be instantiated."); +{% else -%} + return new {{ derivedClass.ClassName }}().init(data); +{% endif -%} + } +{% endfor -%} +{% endif -%} +{% if IsAbstract -%} + throw new Error("The abstract class '{{ ClassName }}' cannot be instantiated."); +{% else -%} + const result = new {{ ClassName }}().init(data); + result.cleanup(this); + return result; +{% endif -%} +{% endif -%} + } + + {% if HasInheritance and SupportsOverrideKeyword %}override {% endif %}toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; +{% if HasIndexerProperty -%} + for (var property in this) { + if (this.hasOwnProperty(property)) + data[property] = this[property]; + } +{% endif -%} +{% if HasDiscriminator -%} + data["{{ BaseDiscriminator }}"] = this.{{ BaseDiscriminator }}; +{% endif -%} +{% for property in Properties -%} + {{ property.ConvertToJavaScriptCode | tab }} +{% endfor -%} +{% if HasInheritance -%} + super.toJSON(data); +{% endif -%} + this.cleanup(data); + return data; + } +{% if GenerateCloneMethod -%} + + clone(): {{ ClassName }} { +{% if IsAbstract -%} + throw new Error("The abstract class '{{ ClassName }}' cannot be instantiated."); +{% else -%} + const json = this.toJSON(); + let result = new {{ ClassName }}(); + result.init(json); + return result; +{% endif -%} + } +{% endif -%} +{% unless HasInheritance -%} + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +{% endunless -%} +} +{% if GenerateConstructorInterface -%} + +{% if HasDescription -%} +/** {{ Description }} */ +{% endif -%} +{% if ExportTypes %}export {% endif %}interface I{{ ClassName }}{{ InterfaceInheritance }} { +{% for property in Properties -%} +{% if property.HasDescription -%} + /** {{ property.Description | strip }} */ +{% endif -%} + readonly {{ property.PropertyName }}{% if property.IsOptional %}?{% endif %}: {{ property.ConstructorInterfaceType }}{{ property.TypePostfix }}; +{% endfor -%} +{% if HasIndexerProperty -%} + + [key: string]: {{ IndexerPropertyValueType }}; +{% endif -%} +} +{% endif -%} \ No newline at end of file diff --git a/frontend/generator/Generator/Templates/ConvertToClass.liquid b/frontend/generator/Generator/Templates/ConvertToClass.liquid new file mode 100644 index 000000000..3a9f9d88c --- /dev/null +++ b/frontend/generator/Generator/Templates/ConvertToClass.liquid @@ -0,0 +1,65 @@ +{% if IsNewableObject -%} +{% if CheckNewableObject -%} +{{ Variable }} = {{ Value }} ? {{ Type }}.fromJSON({{ Value }}{% if HandleReferences -%}, _mappings{% endif %}) : {% if HasDefaultValue %}{{ DefaultValue }}{% else %}{{ NullValue }}{% endif %}; +{% else -%} +{{ Variable }} = {{ Type }}.fromJSON({{ Value }}{% if HandleReferences -%}, _mappings{% endif %}); +{% endif -%} +{% elsif IsArray -%} +if (Array.isArray({{ Value }})) { + {{ Variable }} = [] as any; + for (let item of {{ Value }}) +{% if IsArrayItemNewableObject -%} + {{ Variable }}{% if RequiresStrictPropertyInitialization %}!{% endif %}.push({{ ArrayItemType }}.fromJSON(item{% if HandleReferences %}, _mappings{% endif %})); +{% else -%} +{% if IsArrayItemDate -%} + {{ Variable }}{% if RequiresStrictPropertyInitialization %}!{% endif %}.push(DateTime.parseISO(item)); +{% elsif IsArrayItemDateTime -%} + {{ Variable }}{% if RequiresStrictPropertyInitialization %}!{% endif %}.push(DateTime.parseISO(item)); +{% else -%} + {{ Variable }}{% if RequiresStrictPropertyInitialization %}!{% endif %}.push(item); +{% endif -%} +{% endif -%} +} +{% if NullValue != "undefined" %}else { + {{ Variable }} = {{ NullValue }}; +} +{% endif -%} +{% elsif IsDictionary -%} +if ({{ Value }}) { + {{ Variable }} = {} as any; + for (let key in {{ Value }}) { + if ({{ Value }}.hasOwnProperty(key)) +{% if IsDictionaryValueNewableObject -%} + ({{ Variable }}){% if RequiresStrictPropertyInitialization %}!{% endif %}[key] = {{ Value }}[key] ? {{ DictionaryValueType }}.fromJSON({{ Value }}[key]{% if HandleReferences %}, _mappings{% endif %}) : {% if HasDictionaryValueDefaultValue %}{{ DictionaryValueDefaultValue }}{% else %}{{ NullValue }}{% endif %}; +{% elsif IsDictionaryValueNewableArray -%} + ({{ Variable }}){% if RequiresStrictPropertyInitialization %}!{% endif %}[key] = {{ Value }}[key] ? {{ Value }}[key].map((i: any) => {{ DictionaryValueArrayItemType }}.fromJSON(i{% if HandleReferences %}, _mappings{% endif %})) : {% if HasDictionaryValueDefaultValue %}{{ DictionaryValueDefaultValue }}{% else %}{{ NullValue }}{% endif %}; +{% elsif IsDictionaryValueDate -%} + ({{ Variable }}){% if RequiresStrictPropertyInitialization %}!{% endif %}[key] = {{ Value }}[key] ? DateTime.parseISO({{ Value }}[key].toString()) : {% if HasDictionaryValueDefaultValue %}{{ DictionaryValueDefaultValue }}{% else %}{{ NullValue }}{% endif %}; +{% elsif IsDictionaryValueDateTime -%} + ({{ Variable }}){% if RequiresStrictPropertyInitialization %}!{% endif %}[key] = {{ Value }}[key] ? DateTime.parseISO({{ Value }}[key].toString()) : {% if HasDictionaryValueDefaultValue %}{{ DictionaryValueDefaultValue }}{% else %}{{ NullValue }}{% endif %}; +{% else -%} +{% if HasDictionaryValueDefaultValue or NullValue != "undefined" -%} + ({{ Variable }}){% if RequiresStrictPropertyInitialization %}!{% endif %}[key] = {{ Value }}[key] !== undefined ? {{ Value }}[key] : {% if HasDictionaryValueDefaultValue %}{{ DictionaryValueDefaultValue }}{% else %}{{ NullValue }}{% endif %}; +{% else -%} + ({{ Variable }}){% if RequiresStrictPropertyInitialization %}!{% endif %}[key] = {{ Value }}[key]; +{% endif -%} +{% endif -%} + } +} +{% if NullValue != "undefined" %}else { + {{ Variable }} = {{ NullValue }}; +} +{% endif -%} +{% else -%} + {% if IsDate -%} +{{ Variable }} = {{ Value }} ? DateTime.parseISO({{ Value }}.toString()) : {% if HasDefaultValue %}DateTime.parseISO({{ DefaultValue }}){% else %}{{ NullValue }}{% endif %}; + {% elsif IsDateTime -%} +{{ Variable }} = {{ Value }} ? DateTime.parseISO({{ Value }}.toString()) : {% if HasDefaultValue %}DateTime.parseISO({{ DefaultValue }}){% else %}{{ NullValue }}{% endif %}; + {% else -%} +{% if HasDefaultValue or NullValue != "undefined" -%} +{{ Variable }} = {{ Value }} !== undefined ? {{ Value }} : {% if HasDefaultValue %}{{ DefaultValue }}{% else %}{{ NullValue }}{% endif %}; +{% else -%} +{{ Variable }} = {{ Value }}; +{% endif -%} + {% endif -%} +{% endif -%} \ No newline at end of file diff --git a/frontend/generator/Generator/Templates/ConvertToJavaScript.liquid b/frontend/generator/Generator/Templates/ConvertToJavaScript.liquid new file mode 100644 index 000000000..b775bcc0e --- /dev/null +++ b/frontend/generator/Generator/Templates/ConvertToJavaScript.liquid @@ -0,0 +1,45 @@ +{%- if IsNewableObject -%} +{{ Variable }} = {{ Value }} ? {{ Value }}.toJSON() : {{ NullValue }}; +{%- elsif IsArray -%} +if (Array.isArray({{ Value }})) { + {{ Variable }} = []; + for (let item of {{ Value }}) +{%- if IsArrayItemNewableObject -%} + {{ Variable }}.push(item.toJSON()); +{%- elsif IsArrayItemDate -%} + {{ Variable }}.push({% if UseJsDate %}formatDate(item){% else %}item.toISOString(){% endif %}); +{%- elsif IsArrayItemDateTime -%} + {{ Variable }}.push(item.toISOString()); +{%- else -%} + {{ Variable }}.push(item); +{%- endif -%} +} +{%- elsif IsDictionary -%} +if ({{ Value }}) { + {{ Variable }} = {}; + for (let key in {{ Value }}) { + if ({{ Value }}.hasOwnProperty(key)) +{%- if IsDictionaryValueNewableObject -%} + ({{ Variable }})[key] = {{ Value }}[key] ? {{ Value }}[key].toJSON() : {{ NullValue }}; +{%- elsif IsDictionaryValueDate -%} + ({{ Variable }})[key] = {{ Value }}[key] ? {{ Value }}[key].toISOString() : {{ NullValue }}; +{%- elsif IsDictionaryValueDateTime -%} + ({{ Variable }})[key] = {{ Value }}[key] ? {{ Value }}[key].toISOString() : {{ NullValue }}; +{%- else -%} +{%- if NullValue != "undefined" -%} + ({{ Variable }})[key] = {{ Value }}[key] !== undefined ? {{ Value }}[key] : {{ NullValue }}; +{%- else -%} + ({{ Variable }})[key] = ({{ Value }})[key]; +{%- endif -%} +{%- endif -%} + } +} +{%- elsif IsDate -%} +{{ Variable }} = {{ Value }} ? {{ Value }}.toISOString() : {% if HasDefaultValue %}{{ DefaultValue }}{% else %}{{ NullValue }}{% endif %}; +{%- elsif IsDateTime -%} +{{ Variable }} = {{ Value }} ? {{ Value }}.toISOString() : {% if HasDefaultValue %}{{ DefaultValue }}{% else %}{{ NullValue }}{% endif %}; +{%- elsif NullValue != "undefined" -%} +{{ Variable }} = {{ Value }} !== undefined ? {{ Value }} : {{ NullValue }}; +{%- else -%} +{{ Variable }} = {{ Value }}; +{%- endif %} \ No newline at end of file diff --git a/frontend/generator/Generator/Templates/Enum.StringLiteral.liquid b/frontend/generator/Generator/Templates/Enum.StringLiteral.liquid new file mode 100644 index 000000000..07f546132 --- /dev/null +++ b/frontend/generator/Generator/Templates/Enum.StringLiteral.liquid @@ -0,0 +1,10 @@ +{%- if HasDescription -%} +/** {{ Description }} */ +{%- endif -%} +{%- if ExportTypes %}export {% endif %}type {{ Name }} = {% for enumeration in Enums %}{%- if Enums.first.Value != enumeration.Value %} | {% endif %}{{ enumeration.Value }}{% endfor %}; + +export const {{ Name | uppercamelcase }}Values: ReadonlyArray<{{ Name }}> = [ +{% for enumeration in Enums -%} + {{ enumeration.Value }}{% unless forloop.last %},{% endunless %} +{% endfor -%} +]; \ No newline at end of file diff --git a/frontend/src/app/features/administration/pages/event-consumers/event-consumer.component.ts b/frontend/src/app/features/administration/pages/event-consumers/event-consumer.component.ts index 8883485b6..a505ed5d1 100644 --- a/frontend/src/app/features/administration/pages/event-consumers/event-consumer.component.ts +++ b/frontend/src/app/features/administration/pages/event-consumers/event-consumer.component.ts @@ -9,8 +9,8 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; -import { TooltipDirective } from '@app/shared'; -import { EventConsumerDto, EventConsumersState } from '../../internal'; +import { EventConsumerDto, TooltipDirective } from '@app/shared'; +import { EventConsumersState } from '../../internal'; @Component({ standalone: true, diff --git a/frontend/src/app/features/administration/pages/event-consumers/event-consumers-page.component.ts b/frontend/src/app/features/administration/pages/event-consumers/event-consumers-page.component.ts index 0582d24f4..b9a118045 100644 --- a/frontend/src/app/features/administration/pages/event-consumers/event-consumers-page.component.ts +++ b/frontend/src/app/features/administration/pages/event-consumers/event-consumers-page.component.ts @@ -10,8 +10,8 @@ import { Component, OnInit } from '@angular/core'; import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; import { timer } from 'rxjs'; import { switchMap } from 'rxjs/operators'; -import { DialogModel, LayoutComponent, ListViewComponent, ModalDialogComponent, ModalDirective, ShortcutDirective, SidebarMenuDirective, Subscriptions, SyncWidthDirective, TitleComponent, TooltipDirective, TourStepDirective, TranslatePipe } from '@app/shared'; -import { EventConsumerDto, EventConsumersState } from '../../internal'; +import { DialogModel, EventConsumerDto, LayoutComponent, ListViewComponent, ModalDialogComponent, ModalDirective, ShortcutDirective, SidebarMenuDirective, Subscriptions, SyncWidthDirective, TitleComponent, TooltipDirective, TourStepDirective, TranslatePipe } from '@app/shared'; +import { EventConsumersState } from '../../internal'; import { EventConsumerComponent } from './event-consumer.component'; @Component({ diff --git a/frontend/src/app/features/administration/pages/restore/restore-page.component.ts b/frontend/src/app/features/administration/pages/restore/restore-page.component.ts index 3a6437efe..b925d0630 100644 --- a/frontend/src/app/features/administration/pages/restore/restore-page.component.ts +++ b/frontend/src/app/features/administration/pages/restore/restore-page.component.ts @@ -50,19 +50,20 @@ export class RestorePageComponent { public restore() { const value = this.restoreForm.submit(); + if (!value) { + return; + } - if (value) { - this.restoreForm.submitCompleted(); + this.restoreForm.submitCompleted(); - this.jobsService.postRestore(value) - .subscribe({ - next: () => { - this.dialogs.notifyInfo('i18n:jobs.restoreStarted'); - }, - error: error => { - this.dialogs.notifyError(error); - }, - }); - } + this.jobsService.postRestore(value) + .subscribe({ + next: () => { + this.dialogs.notifyInfo('i18n:jobs.restoreStarted'); + }, + error: error => { + this.dialogs.notifyError(error); + }, + }); } } diff --git a/frontend/src/app/features/administration/pages/users/user-page.component.ts b/frontend/src/app/features/administration/pages/users/user-page.component.ts index 35eea74d2..ab575a86f 100644 --- a/frontend/src/app/features/administration/pages/users/user-page.component.ts +++ b/frontend/src/app/features/administration/pages/users/user-page.component.ts @@ -9,8 +9,8 @@ import { AsyncPipe } from '@angular/common'; import { Component, OnInit } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; -import { ControlErrorsComponent, FormErrorComponent, LayoutComponent, ShortcutDirective, Subscriptions, TitleComponent, TooltipDirective, TranslatePipe } from '@app/shared'; -import { UpsertUserDto, UserDto, UserForm, UsersState } from '../../internal'; +import { ControlErrorsComponent, FormErrorComponent, LayoutComponent, ShortcutDirective, Subscriptions, TitleComponent, TooltipDirective, TranslatePipe, UpdateUserDto } from '@app/shared'; +import { UserDto, UserForm, UsersState } from '../../internal'; @Component({ standalone: true, @@ -66,29 +66,32 @@ export class UserPageComponent implements OnInit { } const value = this.userForm.submit(); + if (!value) { + return; + } + + if (this.user) { + const request = new UpdateUserDto({ ...value }); - if (value) { - if (this.user) { - this.usersState.update(this.user, value) - .subscribe({ - next: user => { - this.userForm.submitCompleted({ newValue: user }); - }, - error: error => { - this.userForm.submitFailed(error); - }, - }); - } else { - this.usersState.create(value) - .subscribe({ - next: () => { - this.back(); - }, - error: error => { - this.userForm.submitFailed(error); - }, - }); - } + this.usersState.update(this.user, request) + .subscribe({ + next: user => { + this.userForm.submitCompleted({ newValue: user }); + }, + error: error => { + this.userForm.submitFailed(error); + }, + }); + } else { + this.usersState.create(value) + .subscribe({ + next: () => { + this.back(); + }, + error: error => { + this.userForm.submitFailed(error); + }, + }); } } diff --git a/frontend/src/app/features/administration/services/event-consumers.service.spec.ts b/frontend/src/app/features/administration/services/event-consumers.service.spec.ts index c291e4967..79b5c321d 100644 --- a/frontend/src/app/features/administration/services/event-consumers.service.spec.ts +++ b/frontend/src/app/features/administration/services/event-consumers.service.spec.ts @@ -8,8 +8,9 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { ApiUrlConfig, Resource, ResourceLinks } from '@app/shared'; -import { EventConsumerDto, EventConsumersDto, EventConsumersService } from './event-consumers.service'; +import { ApiUrlConfig, EventConsumerDto, EventConsumersDto } from '@app/shared'; +import { IResourceDto, ResourceLinkDto } from '@app/shared/model'; +import { EventConsumersService } from './event-consumers.service'; describe('EventConsumersService', () => { beforeEach(() => { @@ -31,7 +32,6 @@ describe('EventConsumersService', () => { it('should make get request to get event consumers', inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => { let eventConsumers: EventConsumersDto; - eventConsumersService.getEventConsumers().subscribe(result => { eventConsumers = result; }); @@ -46,26 +46,28 @@ describe('EventConsumersService', () => { eventConsumerResponse(12), eventConsumerResponse(13), ], + _links: {}, }); - expect(eventConsumers!).toEqual({ - items: [ - createEventConsumer(12), - createEventConsumer(13), - ], - }); + expect(eventConsumers!).toEqual( + new EventConsumersDto({ + items: [ + createEventConsumer(12), + createEventConsumer(13), + ], + _links: {}, + })); })); it('should make put request to start event consumer', inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => { - const resource: Resource = { + const resource: IResourceDto = { _links: { - start: { method: 'PUT', href: 'api/event-consumers/event-consumer123/start' }, + start: new ResourceLinkDto({ method: 'PUT', href: 'api/event-consumers/event-consumer123/start' }), }, }; let eventConsumer: EventConsumerDto; - eventConsumersService.putStart(resource).subscribe(response => { eventConsumer = response; }); @@ -82,14 +84,13 @@ describe('EventConsumersService', () => { it('should make put request to stop event consumer', inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => { - const resource: Resource = { + const resource: IResourceDto = { _links: { - stop: { method: 'PUT', href: 'api/event-consumers/event-consumer123/stop' }, + stop: new ResourceLinkDto({ method: 'PUT', href: 'api/event-consumers/event-consumer123/stop' }), }, }; let eventConsumer: EventConsumerDto; - eventConsumersService.putStop(resource).subscribe(response => { eventConsumer = response; }); @@ -106,14 +107,13 @@ describe('EventConsumersService', () => { it('should make put request to reset event consumer', inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => { - const resource: Resource = { + const resource: IResourceDto = { _links: { - reset: { method: 'PUT', href: 'api/event-consumers/event-consumer123/reset' }, + reset: new ResourceLinkDto({ method: 'PUT', href: 'api/event-consumers/event-consumer123/reset' }), }, }; let eventConsumer: EventConsumerDto; - eventConsumersService.putReset(resource).subscribe(response => { eventConsumer = response; }); @@ -146,17 +146,17 @@ describe('EventConsumersService', () => { }); export function createEventConsumer(id: number, suffix = '') { - const links: ResourceLinks = { - reset: { method: 'PUT', href: `/event-consumers/${id}/reset` }, - }; - const key = `${id}${suffix}`; - return new EventConsumerDto(links, - `event-consumer${id}`, - id, - true, - true, - `failure${key}`, - `position${key}`); -} + return new EventConsumerDto({ + name: `event-consumer${id}`, + position: `position${key}`, + count: id, + isStopped: true, + isResetting: true, + error: `failure${key}`, + _links: { + reset: new ResourceLinkDto({ method: 'PUT', href: `/event-consumers/${id}/reset` }), + }, + }); +} \ No newline at end of file diff --git a/frontend/src/app/features/administration/services/event-consumers.service.ts b/frontend/src/app/features/administration/services/event-consumers.service.ts index 69012ac60..42bf41ae8 100644 --- a/frontend/src/app/features/administration/services/event-consumers.service.ts +++ b/frontend/src/app/features/administration/services/event-consumers.service.ts @@ -9,35 +9,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { ApiUrlConfig, hasAnyLink, pretifyError, Resource, ResourceLinks } from '@app/shared'; - -export class EventConsumerDto implements Resource { - public readonly _links: ResourceLinks; - - public readonly canReset: boolean; - public readonly canStart: boolean; - public readonly canStop: boolean; - - constructor(links: ResourceLinks, - public readonly name: string, - public readonly count: number, - public readonly isStopped?: boolean, - public readonly isResetting?: boolean, - public readonly error?: string, - public readonly position?: string, - ) { - this._links = links; - - this.canReset = hasAnyLink(links, 'reset'); - this.canStart = hasAnyLink(links, 'start'); - this.canStop = hasAnyLink(links, 'stop'); - } -} - -export type EventConsumersDto = Readonly<{ - // The list of event consumers. - items: ReadonlyArray; -}>; +import { ApiUrlConfig, EventConsumerDto, EventConsumersDto, IResourceDto, pretifyError } from '@app/shared'; @Injectable() export class EventConsumersService { @@ -52,61 +24,44 @@ export class EventConsumersService { return this.http.get(url).pipe( map(body => { - return parseEventConsumers(body); + return EventConsumersDto.fromJSON(body); }), pretifyError('i18n:eventConsumers.loadFailed')); } - public putStart(eventConsumer: Resource): Observable { + public putStart(eventConsumer: IResourceDto): Observable { const link = eventConsumer._links['start']; const url = this.apiUrl.buildUrl(link.href); return this.http.request(link.method, url).pipe( map(body => { - return parseEventConsumer(body); + return EventConsumerDto.fromJSON(body); }), pretifyError('i18n:eventConsumers.startFailed')); } - public putStop(eventConsumer: Resource): Observable { + public putStop(eventConsumer: IResourceDto): Observable { const link = eventConsumer._links['stop']; const url = this.apiUrl.buildUrl(link.href); return this.http.request(link.method, url).pipe( map(body => { - return parseEventConsumer(body); + return EventConsumerDto.fromJSON(body); }), pretifyError('i18n:eventConsumers.stopFailed')); } - public putReset(eventConsumer: Resource): Observable { + public putReset(eventConsumer: IResourceDto): Observable { const link = eventConsumer._links['reset']; const url = this.apiUrl.buildUrl(link.href); return this.http.request(link.method, url).pipe( map(body => { - return parseEventConsumer(body); + return EventConsumerDto.fromJSON(body); }), pretifyError('i18n:eventConsumers.resetFailed')); } } - -function parseEventConsumers(response: { items: any[] } & Resource): EventConsumersDto { - const { items: list } = response; - const items = list.map(parseEventConsumer); - - return { items }; -} - -function parseEventConsumer(response: any): EventConsumerDto { - return new EventConsumerDto(response._links, - response.name, - response.count, - response.isStopped, - response.isResetting, - response.error, - response.position); -} diff --git a/frontend/src/app/features/administration/services/users.service.spec.ts b/frontend/src/app/features/administration/services/users.service.spec.ts index 19403b7d0..55c5f9e19 100644 --- a/frontend/src/app/features/administration/services/users.service.spec.ts +++ b/frontend/src/app/features/administration/services/users.service.spec.ts @@ -8,8 +8,9 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { ApiUrlConfig, Resource, ResourceLinks } from '@app/shared'; -import { UserDto, UsersDto, UsersService } from './users.service'; +import { ApiUrlConfig, UserDto, UsersDto } from '@app/shared'; +import { CreateUserDto, IResourceDto, ResourceLinkDto, UpdateUserDto } from '@app/shared/model'; +import { UsersService } from './users.service'; describe('UsersService', () => { beforeEach(() => { @@ -31,7 +32,6 @@ describe('UsersService', () => { it('should make get request to get many users', inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => { let users: UsersDto; - userManagementService.getUsers(20, 30).subscribe(result => { users = result; }); @@ -47,22 +47,23 @@ describe('UsersService', () => { userResponse(12), userResponse(13), ], + _links: {}, }); - expect(users!).toEqual({ - total: 100, - items: [ - createUser(12), - createUser(13), - ], - canCreate: false, - }); + expect(users!).toEqual( + new UsersDto({ + total: 100, + items: [ + createUser(12), + createUser(13), + ], + _links: {}, + })); })); it('should make get request with query to get many users', inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => { let users: UsersDto; - userManagementService.getUsers(20, 30, 'my-query').subscribe(result => { users = result; }); @@ -78,22 +79,23 @@ describe('UsersService', () => { userResponse(12), userResponse(13), ], + _links: {}, }); - expect(users!).toEqual({ - total: 100, - items: [ - createUser(12), - createUser(13), - ], - canCreate: false, - }); + expect(users!).toEqual( + new UsersDto({ + total: 100, + items: [ + createUser(12), + createUser(13), + ], + _links: {}, + })); })); it('should make get request to get single user', inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => { let user: UserDto; - userManagementService.getUser('123').subscribe(result => { user = result; }); @@ -110,10 +112,14 @@ describe('UsersService', () => { it('should make post request to create user', inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => { - const dto = { email: 'mail@squidex.io', displayName: 'Squidex User', permissions: ['Permission1'], password: 'password' }; + const dto = new CreateUserDto({ + email: 'mail@squidex.io', + displayName: 'Squidex User', + permissions: ['Permission1'], + password: 'password', + }); let user: UserDto; - userManagementService.postUser(dto).subscribe(result => { user = result; }); @@ -130,16 +136,20 @@ describe('UsersService', () => { it('should make put request to update user', inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => { - const dto = { email: 'mail@squidex.io', displayName: 'Squidex User', permissions: ['Permission1'], password: 'password' }; + const dto = new UpdateUserDto({ + email: 'mail@squidex.io', + displayName: 'Squidex User', + permissions: ['Permission1'], + password: 'password', + }); - const resource: Resource = { + const resource: IResourceDto = { _links: { - update: { method: 'PUT', href: 'api/user-management/123' }, + update: new ResourceLinkDto({ method: 'PUT', href: 'api/user-management/123' }), }, }; let user: UserDto; - userManagementService.putUser(resource, dto).subscribe(result => { user = result; }); @@ -156,14 +166,13 @@ describe('UsersService', () => { it('should make put request to lock user', inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => { - const resource: Resource = { + const resource: IResourceDto = { _links: { - lock: { method: 'PUT', href: 'api/user-management/123/lock' }, + lock: new ResourceLinkDto({ method: 'PUT', href: 'api/user-management/123/lock' }), }, }; let user: UserDto; - userManagementService.lockUser(resource).subscribe(result => { user = result; }); @@ -180,14 +189,13 @@ describe('UsersService', () => { it('should make put request to unlock user', inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => { - const resource: Resource = { + const resource: IResourceDto = { _links: { - unlock: { method: 'PUT', href: 'api/user-management/123/unlock' }, + unlock: new ResourceLinkDto({ method: 'PUT', href: 'api/user-management/123/unlock' }), }, }; let user: UserDto; - userManagementService.unlockUser(resource).subscribe(result => { user = result; }); @@ -204,9 +212,9 @@ describe('UsersService', () => { it('should make delete request to delete user', inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => { - const resource: Resource = { + const resource: IResourceDto = { _links: { - delete: { method: 'DELETE', href: 'api/user-management/123' }, + delete: new ResourceLinkDto({ method: 'DELETE', href: 'api/user-management/123' }), }, }; @@ -241,18 +249,18 @@ describe('UsersService', () => { }); export function createUser(id: number, suffix = '') { - const links: ResourceLinks = { - update: { method: 'PUT', href: `/users/${id}` }, - }; - const key = `${id}${suffix}`; - return new UserDto(links, - `${id}`, - `user${key}@domain.com`, - `user${key}`, - [ + return new UserDto({ + id: `${id}`, + email: `user${key}@domain.com`, + displayName: `user${key}`, + permissions: [ `Permission${key}`, ], - true); -} + isLocked: true, + _links: { + update: new ResourceLinkDto({ method: 'PUT', href: `/users/${id}` }), + }, + }); +} \ No newline at end of file diff --git a/frontend/src/app/features/administration/services/users.service.ts b/frontend/src/app/features/administration/services/users.service.ts index 24596bb0f..ce989e909 100644 --- a/frontend/src/app/features/administration/services/users.service.ts +++ b/frontend/src/app/features/administration/services/users.service.ts @@ -9,56 +9,8 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { ApiUrlConfig, hasAnyLink, pretifyError, Resource, ResourceLinks, StringHelper } from '@app/shared'; - -export class UserDto implements Resource { - public readonly _links: ResourceLinks; - - public readonly canDelete: boolean; - public readonly canLock: boolean; - public readonly canUnlock: boolean; - public readonly canUpdate: boolean; - - constructor(links: ResourceLinks, - public readonly id: string, - public readonly email: string, - public readonly displayName: string, - public readonly permissions: ReadonlyArray = [], - public readonly isLocked?: boolean, - ) { - this._links = links; - - this.canDelete = hasAnyLink(links, 'delete'); - this.canLock = hasAnyLink(links, 'lock'); - this.canUnlock = hasAnyLink(links, 'unlock'); - this.canUpdate = hasAnyLink(links, 'update'); - } -} - -export type UsersDto = Readonly<{ - // The list of users. - items: ReadonlyArray; - - // The number of users. - total: number; - - // True, if the user has permissions to create a user. - canCreate?: boolean; -}>; - -export type UpsertUserDto = Readonly<{ - // The email address of the user. - email: string; - - // The display name. - displayName?: string; - - // The permissions as in the dot-notation. - permissions?: ReadonlyArray; - - // The password (confirm is only used in the UI). - password?: string; -}>; +import { ApiUrlConfig, CreateUserDto, IResourceDto, pretifyError, StringHelper, UpdateUserDto, UserDto, UsersDto } from '@app/shared'; +export { UserDto, UsersDto }; @Injectable() export class UsersService { @@ -73,7 +25,7 @@ export class UsersService { return this.http.get(url).pipe( map(body => { - return parseUsers(body); + return UsersDto.fromJSON(body); }), pretifyError('i18n:users.loadFailed')); } @@ -83,58 +35,58 @@ export class UsersService { return this.http.get(url).pipe( map(body => { - return parseUser(body); + return UserDto.fromJSON(body); }), pretifyError('i18n:users.loadUserFailed')); } - public postUser(dto: UpsertUserDto): Observable { + public postUser(dto: CreateUserDto): Observable { const url = this.apiUrl.buildUrl('api/user-management'); - return this.http.post(url, dto).pipe( + return this.http.post(url, dto.toJSON()).pipe( map(body => { - return parseUser(body); + return UserDto.fromJSON(body); }), pretifyError('i18n:users.createFailed')); } - public putUser(user: Resource, dto: Partial): Observable { + public putUser(user: IResourceDto, dto: UpdateUserDto): Observable { const link = user._links['update']; const url = this.apiUrl.buildUrl(link.href); - return this.http.request(link.method, url, { body: dto }).pipe( + return this.http.request(link.method, url, { body: dto.toJSON() }).pipe( map(body => { - return parseUser(body); + return UserDto.fromJSON(body); }), pretifyError('i18n:users.updateFailed')); } - public lockUser(user: Resource): Observable { + public lockUser(user: IResourceDto): Observable { const link = user._links['lock']; const url = this.apiUrl.buildUrl(link.href); return this.http.request(link.method, url).pipe( map(body => { - return parseUser(body); + return UserDto.fromJSON(body); }), pretifyError('i18n:users.lockFailed')); } - public unlockUser(user: Resource): Observable { + public unlockUser(user: IResourceDto): Observable { const link = user._links['unlock']; const url = this.apiUrl.buildUrl(link.href); return this.http.request(link.method, url).pipe( map(body => { - return parseUser(body); + return UserDto.fromJSON(body); }), pretifyError('i18n:users.unlockFailed')); } - public deleteUser(user: Resource): Observable { + public deleteUser(user: IResourceDto): Observable { const link = user._links['delete']; const url = this.apiUrl.buildUrl(link.href); @@ -142,23 +94,4 @@ export class UsersService { return this.http.request(link.method, url).pipe( pretifyError('i18n:users.deleteFailed')); } -} - -function parseUsers(response: { items: any[]; total: number } & Resource): UsersDto { - const { items: list, total, _links } = response; - const items = list.map(parseUser); - - const canCreate = hasAnyLink(_links, 'create'); - - return { items, total, canCreate }; -} - -function parseUser(response: any) { - return new UserDto( - response._links, - response.id, - response.email, - response.displayName, - response.permissions, - response.isLocked); -} +} \ No newline at end of file diff --git a/frontend/src/app/features/administration/state/event-consumers.state.spec.ts b/frontend/src/app/features/administration/state/event-consumers.state.spec.ts index ad4c897d0..2191b18c7 100644 --- a/frontend/src/app/features/administration/state/event-consumers.state.spec.ts +++ b/frontend/src/app/features/administration/state/event-consumers.state.spec.ts @@ -7,7 +7,7 @@ import { of, onErrorResumeNextWith, throwError } from 'rxjs'; import { IMock, It, Mock, Times } from 'typemoq'; -import { DialogService } from '@app/shared'; +import { DialogService, EventConsumersDto } from '@app/shared'; import { EventConsumersService } from '../internal'; import { createEventConsumer } from '../services/event-consumers.service.spec'; import { EventConsumersState } from './event-consumers.state'; @@ -34,7 +34,7 @@ describe('EventConsumersState', () => { describe('Loading', () => { it('should load event consumers', () => { eventConsumersService.setup(x => x.getEventConsumers()) - .returns(() => of({ items: [eventConsumer1, eventConsumer2] })).verifiable(); + .returns(() => of(new EventConsumersDto({ items: [eventConsumer1, eventConsumer2], _links: {} }))).verifiable(); eventConsumersState.load().subscribe(); @@ -56,7 +56,7 @@ describe('EventConsumersState', () => { it('should show notification on load if reload is true', () => { eventConsumersService.setup(x => x.getEventConsumers()) - .returns(() => of({ items: [eventConsumer1, eventConsumer2] })).verifiable(); + .returns(() => of(new EventConsumersDto({ items: [eventConsumer1, eventConsumer2], _links: {} }))).verifiable(); eventConsumersState.load(true).subscribe(); @@ -80,7 +80,7 @@ describe('EventConsumersState', () => { describe('Updates', () => { beforeEach(() => { eventConsumersService.setup(x => x.getEventConsumers()) - .returns(() => of({ items: [eventConsumer1, eventConsumer2] })).verifiable(); + .returns(() => of(new EventConsumersDto({ items: [eventConsumer1, eventConsumer2], _links: {} }))).verifiable(); eventConsumersState.load().subscribe(); }); diff --git a/frontend/src/app/features/administration/state/event-consumers.state.ts b/frontend/src/app/features/administration/state/event-consumers.state.ts index c0e6b779a..2e1d075fd 100644 --- a/frontend/src/app/features/administration/state/event-consumers.state.ts +++ b/frontend/src/app/features/administration/state/event-consumers.state.ts @@ -8,8 +8,8 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { finalize, tap } from 'rxjs/operators'; -import { debug, DialogService, LoadingState, shareSubscribed, State } from '@app/shared'; -import { EventConsumerDto, EventConsumersService } from '../services/event-consumers.service'; +import { debug, DialogService, EventConsumerDto, LoadingState, shareSubscribed, State } from '@app/shared'; +import { EventConsumersService } from '../services/event-consumers.service'; interface Snapshot extends LoadingState { // The list of event consumers. diff --git a/frontend/src/app/features/administration/state/users.forms.ts b/frontend/src/app/features/administration/state/users.forms.ts index 061affee8..0a2c60e18 100644 --- a/frontend/src/app/features/administration/state/users.forms.ts +++ b/frontend/src/app/features/administration/state/users.forms.ts @@ -6,10 +6,9 @@ */ import { UntypedFormControl, Validators } from '@angular/forms'; -import { ExtendedFormGroup, Form, ValidatorsEx } from '@app/shared'; -import { UpsertUserDto, UserDto } from '../services/users.service'; +import { CreateUserDto, ExtendedFormGroup, Form, UserDto, ValidatorsEx } from '@app/shared'; -export class UserForm extends Form { +export class UserForm extends Form { constructor() { super(new ExtendedFormGroup({ email: new UntypedFormControl('', [ @@ -52,6 +51,6 @@ export class UserForm extends Form { protected transformSubmit(value: any) { const permissions = value['permissions'].split('\n').defined(); - return { ...value, permissions }; + return new CreateUserDto({ ...value, permissions }); } } diff --git a/frontend/src/app/features/administration/state/users.state.spec.ts b/frontend/src/app/features/administration/state/users.state.spec.ts index 6dedd4bd8..8f972cff4 100644 --- a/frontend/src/app/features/administration/state/users.state.spec.ts +++ b/frontend/src/app/features/administration/state/users.state.spec.ts @@ -7,8 +7,8 @@ import { firstValueFrom, of, onErrorResumeNextWith, throwError } from 'rxjs'; import { IMock, It, Mock, Times } from 'typemoq'; -import { DialogService } from '@app/shared'; -import { UpsertUserDto, UsersService } from '../internal'; +import { CreateUserDto, DialogService, UpdateUserDto, UsersDto } from '@app/shared'; +import { UsersService } from '../internal'; import { createUser } from '../services/users.service.spec'; import { UsersState } from './users.state'; @@ -36,7 +36,7 @@ describe('UsersState', () => { describe('Loading', () => { it('should load users', () => { usersService.setup(x => x.getUsers(10, 0, undefined)) - .returns(() => of({ items: [user1, user2], total: 200 })).verifiable(); + .returns(() => of(new UsersDto({ items: [user1, user2], total: 200, _links: {} }))).verifiable(); usersState.load().subscribe(); @@ -59,7 +59,7 @@ describe('UsersState', () => { it('should show notification on load if reload is true', () => { usersService.setup(x => x.getUsers(10, 0, undefined)) - .returns(() => of({ items: [user1, user2], total: 200 })).verifiable(); + .returns(() => of(new UsersDto({ items: [user1, user2], total: 200, _links: {} }))).verifiable(); usersState.load(true).subscribe(); @@ -70,7 +70,7 @@ describe('UsersState', () => { it('should load with new pagination if paging', () => { usersService.setup(x => x.getUsers(10, 10, undefined)) - .returns(() => of({ items: [], total: 200 })).verifiable(); + .returns(() => of(new UsersDto({ items: [], total: 200, _links: {} }))).verifiable(); usersState.page({ page: 1, pageSize: 10 }).subscribe(); @@ -79,7 +79,7 @@ describe('UsersState', () => { it('should load with query if searching', () => { usersService.setup(x => x.getUsers(10, 0, 'my-query')) - .returns(() => of({ items: [], total: 0 })).verifiable(); + .returns(() => of(new UsersDto({ items: [], total: 0, _links: {} }))).verifiable(); usersState.search('my-query').subscribe(); @@ -90,7 +90,7 @@ describe('UsersState', () => { describe('Updates', () => { beforeEach(() => { usersService.setup(x => x.getUsers(10, 0, undefined)) - .returns(() => of({ items: [user1, user2], total: 200 })).verifiable(); + .returns(() => of(new UsersDto({ items: [user1, user2], total: 200, _links: {} }))).verifiable(); usersState.load().subscribe(); }); @@ -130,7 +130,7 @@ describe('UsersState', () => { }); it('should add user to snapshot if created', () => { - const request: UpsertUserDto = { ...newUser, password: 'password' } as any; + const request = new CreateUserDto({ ...newUser, password: 'password' }); usersService.setup(x => x.postUser(request)) .returns(() => of(newUser)).verifiable(); @@ -142,9 +142,8 @@ describe('UsersState', () => { }); it('should update user if updated', () => { - const request: Partial = {}; - const updated = createUser(2, '_new'); + const request = new UpdateUserDto({ ...updated }); usersService.setup(x => x.putUser(user2, request)) .returns(() => of(updated)).verifiable(); @@ -187,10 +186,10 @@ describe('UsersState', () => { }); it('should truncate users if page size reached', () => { - const request: UpsertUserDto = { ...newUser, password: 'password' } as any; + const request = new CreateUserDto({ ...newUser, password: 'password' }); usersService.setup(x => x.getUsers(2, 0, undefined)) - .returns(() => of({ items: [user1, user2], total: 200 })).verifiable(); + .returns(() => of(new UsersDto({ items: [user1, user2], total: 200, _links: {} }))).verifiable(); usersService.setup(x => x.postUser(request)) .returns(() => of(newUser)).verifiable(); @@ -205,8 +204,8 @@ describe('UsersState', () => { describe('Selection', () => { beforeEach(() => { - usersService.setup(x => x.getUsers(10, 0, undefined)) - .returns(() => of({ items: [user1, user2], total: 200 })).verifiable(Times.atLeastOnce()); + usersService.setup(x => x.getUsers(10, 0, undefined)) + .returns(() => of(new UsersDto({ items: [user1, user2], total: 200, _links: {} }))).verifiable(Times.atLeastOnce()); usersState.load().subscribe(); usersState.select(user2.id).subscribe(); @@ -219,7 +218,7 @@ describe('UsersState', () => { ]; usersService.setup(x => x.getUsers(10, 0, undefined)) - .returns(() => of({ items: newUsers, total: 200 })); + .returns(() => of(new UsersDto({ items: newUsers, total: 200, _links: {} }))); usersState.load().subscribe(); @@ -227,9 +226,8 @@ describe('UsersState', () => { }); it('should update selected user if updated', () => { - const request = {}; - const updated = createUser(2, '_new'); + const request = new UpdateUserDto({ ...updated }); usersService.setup(x => x.putUser(user2, request)) .returns(() => of(updated)).verifiable(); diff --git a/frontend/src/app/features/administration/state/users.state.ts b/frontend/src/app/features/administration/state/users.state.ts index a3c1605bf..65b6b3d1f 100644 --- a/frontend/src/app/features/administration/state/users.state.ts +++ b/frontend/src/app/features/administration/state/users.state.ts @@ -11,8 +11,8 @@ import { Injectable } from '@angular/core'; import '@app/framework/utils/rxjs-extensions'; import { EMPTY, Observable, of } from 'rxjs'; import { catchError, finalize, tap } from 'rxjs/operators'; -import { debug, DialogService, getPagingInfo, ListState, shareSubscribed, State } from '@app/shared'; -import { UpsertUserDto, UserDto, UsersService } from '../services/users.service'; +import { CreateUserDto, debug, DialogService, getPagingInfo, ListState, shareSubscribed, State, UpdateUserDto, UserDto } from '@app/shared'; +import { UsersService } from '../services/users.service'; interface Snapshot extends ListState { // The current users. @@ -104,7 +104,7 @@ export class UsersState extends State { pageSize, pageSize * page, query).pipe( - tap(({ total, items: users, canCreate }) => { + tap(({ canCreate, items: users, total }) => { if (isReload) { this.dialogs.notifyInfo('i18n:users.reloaded'); } @@ -132,7 +132,7 @@ export class UsersState extends State { shareSubscribed(this.dialogs)); } - public create(request: UpsertUserDto): Observable { + public create(request: CreateUserDto): Observable { return this.usersService.postUser(request).pipe( tap(created => { this.next(s => { @@ -144,7 +144,7 @@ export class UsersState extends State { shareSubscribed(this.dialogs, { silent: true })); } - public update(user: UserDto, request: Partial): Observable { + public update(user: UserDto, request: UpdateUserDto): Observable { return this.usersService.putUser(user, request).pipe( tap(updated => { this.replaceUser(updated); diff --git a/frontend/src/app/features/assets/pages/asset-tag-dialog.component.ts b/frontend/src/app/features/assets/pages/asset-tag-dialog.component.ts index e4484407a..1a43dacaa 100644 --- a/frontend/src/app/features/assets/pages/asset-tag-dialog.component.ts +++ b/frontend/src/app/features/assets/pages/asset-tag-dialog.component.ts @@ -52,7 +52,6 @@ export class AssetTagDialogComponent implements OnInit { public renameAssetTag() { const value = this.editForm.submit(); - if (!value) { return; } diff --git a/frontend/src/app/features/content/pages/calendar/calendar-page.component.ts b/frontend/src/app/features/content/pages/calendar/calendar-page.component.ts index 4e4f51341..a0d07348d 100644 --- a/frontend/src/app/features/content/pages/calendar/calendar-page.component.ts +++ b/frontend/src/app/features/content/pages/calendar/calendar-page.component.ts @@ -9,7 +9,7 @@ import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, HostListener, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { RouterLink } from '@angular/router'; -import { AppsState, ConfirmClickDirective, ContentDto, ContentsService, ContentStatusComponent, CopyDirective, DateTime, DialogModel, FullDateTimePipe, getContentValue, LanguageDto, LanguagesState, LayoutComponent, LocalizerService, ModalDialogComponent, ModalDirective, ResourceLoaderService, TitleComponent, TooltipDirective, TranslatePipe, UserNameRefPipe, UserPictureRefPipe } from '@app/shared'; +import { AppLanguageDto, AppsState, ConfirmClickDirective, ContentDto, ContentsService, ContentStatusComponent, CopyDirective, DateTime, DialogModel, FullDateTimePipe, getContentValue, LanguagesState, LayoutComponent, LocalizerService, ModalDialogComponent, ModalDirective, ResourceLoaderService, TitleComponent, TooltipDirective, TranslatePipe, UserNameRefPipe, UserPictureRefPipe } from '@app/shared'; declare const tui: any; @@ -39,7 +39,7 @@ type ViewMode = 'day' | 'week' | 'month'; }) export class CalendarPageComponent implements AfterViewInit, OnDestroy, OnInit { private calendar: any; - private language!: LanguageDto; + private language!: AppLanguageDto; @ViewChild('calendarContainer', { static: false }) public calendarContainer!: ElementRef; diff --git a/frontend/src/app/features/content/pages/content/content-event.component.ts b/frontend/src/app/features/content/pages/content/content-event.component.ts index 42ebcc4b9..d86df7835 100644 --- a/frontend/src/app/features/content/pages/content/content-event.component.ts +++ b/frontend/src/app/features/content/pages/content/content-event.component.ts @@ -42,9 +42,9 @@ export class ContentEventComponent { public ngOnChanges(changes: TypedSimpleChanges) { if (changes.event) { this.canLoadOrCompare = - (this.event.eventType === 'ContentUpdatedEvent' || - this.event.eventType === 'ContentCreatedEventV2') && - !this.event.version.eq(this.content.version); + (this.event.eventType === 'ContentUpdatedEvent' || + this.event.eventType === 'ContentCreatedEventV2') && + this.event.version !== this.content.version; } } } diff --git a/frontend/src/app/features/content/pages/content/content-page.component.ts b/frontend/src/app/features/content/pages/content/content-page.component.ts index 54632ee5f..71dbfbc35 100644 --- a/frontend/src/app/features/content/pages/content/content-page.component.ts +++ b/frontend/src/app/features/content/pages/content/content-page.component.ts @@ -252,7 +252,6 @@ export class ContentPageComponent implements CanComponentDeactivate, OnInit { private saveContent(publish: boolean, navigationMode: SaveNavigationMode) { const value = this.contentForm.submit(); - if (!value) { this.contentForm.submitFailed('i18n:contents.contentNotValid', false); return; @@ -365,10 +364,10 @@ export class ContentPageComponent implements CanComponentDeactivate, OnInit { this.loadVersion(null, false); } - public loadVersion(version: Version | null, compare: boolean) { + public loadVersion(version: number | null, compare: boolean) { const content = this.content; - if (!content || version === null || version.eq(content.version)) { + if (!content || version === null || version != content.version) { this.contentFormCompare = null; this.contentVersion = null; this.loadContent(content?.data || {}, true); diff --git a/frontend/src/app/features/content/pages/contents/contents-page.component.ts b/frontend/src/app/features/content/pages/contents/contents-page.component.ts index 0748a2186..868ddc376 100644 --- a/frontend/src/app/features/content/pages/contents/contents-page.component.ts +++ b/frontend/src/app/features/content/pages/contents/contents-page.component.ts @@ -13,7 +13,7 @@ import { FormsModule } from '@angular/forms'; import { ActivatedRoute, Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; import { Observable } from 'rxjs'; import { distinctUntilChanged, map, shareReplay, switchMap, take, tap } from 'rxjs/operators'; -import { AppLanguageDto, AppsState, ConfirmClickDirective, ContentDto, ContentListCellDirective, ContentListCellResizeDirective, ContentListHeaderComponent, ContentListWidthDirective, ContentsService, ContentsState, ContentStatusComponent, contentsTranslationStatus, ContributorsState, defined, DropdownMenuComponent, getTableConfig, KeysPipe, LanguageSelectorComponent, LanguagesState, LayoutComponent, ListViewComponent, LocalStoreService, ModalDirective, ModalModel, ModalPlacementDirective, NotifoComponent, PagerComponent, Queries, Query, QuerySynchronizer, Router2State, SchemaDto, SchemasService, SchemasState, SearchFormComponent, Settings, ShortcutDirective, SidebarMenuDirective, Subscriptions, switchSafe, SyncWidthDirective, TableSettings, TempService, TitleComponent, TooltipDirective, TourStepDirective, TranslatePipe, TranslationStatus, UIState } from '@app/shared'; +import { AppLanguageDto, AppsState, ConfirmClickDirective, ContentDto, ContentListCellDirective, ContentListCellResizeDirective, ContentListHeaderComponent, ContentListWidthDirective, ContentsService, ContentsState, ContentStatusComponent, contentsTranslationStatus, ContributorsState, defined, DropdownMenuComponent, getTableConfig, KeysPipe, LanguageSelectorComponent, LanguagesState, LayoutComponent, ListViewComponent, LocalStoreService, ModalDirective, ModalModel, ModalPlacementDirective, NotifoComponent, PagerComponent, Queries, Query, QuerySynchronizer, Router2State, SchemaDto, SchemasService, SchemasState, SearchFormComponent, Settings, ShortcutDirective, SidebarMenuDirective, Subscriptions, switchSafe, SyncWidthDirective, TableSettings, TempService, TitleComponent, TooltipDirective, TourStepDirective, TranslatePipe, TranslationStatuses, UIState } from '@app/shared'; import { DueTimeSelectorComponent } from '../../shared/due-time-selector.component'; import { ContentComponent } from '../../shared/list/content.component'; import { CustomViewEditorComponent } from './custom-view-editor.component'; @@ -82,7 +82,7 @@ export class ContentsPageComponent implements OnInit { public language!: AppLanguageDto; public languages!: ReadonlyArray; - public translationStatus?: TranslationStatus; + public translationStatus?: TranslationStatuses; public get disableScheduler() { return this.appsState.snapshot.selectedSettings?.hideScheduler === true; diff --git a/frontend/src/app/features/content/shared/due-time-selector.component.ts b/frontend/src/app/features/content/shared/due-time-selector.component.ts index e1eadaf1d..5472c0000 100644 --- a/frontend/src/app/features/content/shared/due-time-selector.component.ts +++ b/frontend/src/app/features/content/shared/due-time-selector.component.ts @@ -8,7 +8,7 @@ import { booleanAttribute, Component, Input } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { Observable, of, Subject } from 'rxjs'; -import { DateTimeEditorComponent, DialogModel, FocusOnInitDirective, ModalDialogComponent, ModalDirective, TooltipDirective, TranslatePipe } from '@app/shared'; +import { DateTime, DateTimeEditorComponent, DialogModel, FocusOnInitDirective, ModalDialogComponent, ModalDirective, TooltipDirective, TranslatePipe } from '@app/shared'; const OPTION_IMMEDIATELY = 'Immediately'; @@ -28,7 +28,7 @@ const OPTION_IMMEDIATELY = 'Immediately'; ], }) export class DueTimeSelectorComponent { - private dueTimeResult?: Subject; + private dueTimeResult?: Subject; @Input({ transform: booleanAttribute }) public disabled?: boolean | null; @@ -38,13 +38,13 @@ export class DueTimeSelectorComponent { public dueTimeAction: string | null = ''; public dueTimeMode = OPTION_IMMEDIATELY; - public selectDueTime(action: string): Observable { + public selectDueTime(action: string): Observable { if (this.disabled) { - return of(null); + return of(undefined); } this.dueTimeAction = action; - this.dueTimeResult = new Subject(); + this.dueTimeResult = new Subject(); this.dueTimeDialog.show(); return this.dueTimeResult; @@ -53,7 +53,7 @@ export class DueTimeSelectorComponent { public confirmStatusChange() { const result = this.dueTimeMode === OPTION_IMMEDIATELY ? null : this.dueTime; - this.dueTimeResult?.next(result); + this.dueTimeResult?.next(result ? DateTime.parseISO(result) : undefined); this.dueTimeResult?.complete(); this.cancelStatusChange(); diff --git a/frontend/src/app/features/content/shared/forms/array-editor.component.ts b/frontend/src/app/features/content/shared/forms/array-editor.component.ts index a9d00710f..640b8c3cd 100644 --- a/frontend/src/app/features/content/shared/forms/array-editor.component.ts +++ b/frontend/src/app/features/content/shared/forms/array-editor.component.ts @@ -89,7 +89,7 @@ export class ArrayEditorComponent { public ngOnChanges(changes: TypedSimpleChanges) { if (changes.formModel) { - const maxItems = (this.formModel.field.properties as any)['maxItems'] || Number.MAX_VALUE; + const maxItems = this.formModel.field.rawProperties['maxItems'] || Number.MAX_VALUE; if (Types.is(this.formModel.field.properties, ComponentsFieldPropertiesDto)) { this.schemasList = this.formModel.field.properties.schemaIds?.map(x => this.formModel.globals.schemas[x]).defined().sortedByString(x => x.displayName) || []; diff --git a/frontend/src/app/features/content/shared/forms/array-item.component.ts b/frontend/src/app/features/content/shared/forms/array-item.component.ts index 61e9b8e14..854335c29 100644 --- a/frontend/src/app/features/content/shared/forms/array-item.component.ts +++ b/frontend/src/app/features/content/shared/forms/array-item.component.ts @@ -9,7 +9,7 @@ import { AsyncPipe } from '@angular/common'; import { booleanAttribute, ChangeDetectionStrategy, Component, EventEmitter, Input, numberAttribute, Output, QueryList, ViewChildren } from '@angular/core'; import { Observable, of } from 'rxjs'; import { map } from 'rxjs/operators'; -import { AppLanguageDto, ComponentForm, EditContentForm, FieldDto, FieldFormatter, FormHintComponent, IfOnceDirective, invalid$, ObjectFormBase, RootFieldDto, TooltipDirective, TranslatePipe, TypedSimpleChanges, Types, valueProjection$ } from '@app/shared'; +import { AppLanguageDto, ComponentForm, EditContentForm, FieldDto, FieldFormatter, FormHintComponent, IfOnceDirective, invalid$, NestedFieldDto, ObjectFormBase, TooltipDirective, TranslatePipe, TypedSimpleChanges, Types, valueProjection$ } from '@app/shared'; import { ComponentSectionComponent } from './component-section.component'; @Component({ @@ -152,7 +152,7 @@ function getTitle(formModel: ObjectFormBase) { let valueLength = 0; - function addFields(fields: ReadonlyArray) { + function addFields(fields: ReadonlyArray) { for (const field of fields) { const fieldValue = value[field.name]; @@ -178,7 +178,7 @@ function getTitle(formModel: ObjectFormBase) { valueLength += formatted.length; addFields(formModel.schema.fields); - } else if (Types.is(formModel.field, RootFieldDto)) { + } else if (Types.is(formModel.field, FieldDto) && formModel.field.nested) { addFields(formModel.field.nested); } diff --git a/frontend/src/app/features/content/shared/forms/content-field.component.ts b/frontend/src/app/features/content/shared/forms/content-field.component.ts index 7ff796528..2a822070e 100644 --- a/frontend/src/app/features/content/shared/forms/content-field.component.ts +++ b/frontend/src/app/features/content/shared/forms/content-field.component.ts @@ -8,7 +8,7 @@ import { AsyncPipe } from '@angular/common'; import { booleanAttribute, Component, EventEmitter, HostBinding, inject, Input, numberAttribute, Optional, Output } from '@angular/core'; import { Observable } from 'rxjs'; -import { AppLanguageDto, AppsState, changed$, CommentsState, disabled$, EditContentForm, FieldForm, FocusMarkerComponent, invalid$, LocalStoreService, SchemaDto, Settings, TooltipDirective, TranslationsService, TypedSimpleChanges, UIOptions } from '@app/shared'; +import { AppLanguageDto, AppsState, changed$, CommentsState, disabled$, EditContentForm, FieldForm, FocusMarkerComponent, invalid$, LocalStoreService, SchemaDto, Settings, TooltipDirective, TranslateDto, TranslationsService, TypedSimpleChanges, UIOptions } from '@app/shared'; import { FieldCopyButtonComponent } from './field-copy-button.component'; import { FieldEditorComponent } from './field-editor.component'; import { FieldLanguagesComponent } from './field-languages.component'; @@ -158,7 +158,11 @@ export class ContentFieldComponent { return; } - const request = { text, sourceLanguage, targetLanguage }; + const request = new TranslateDto({ + sourceLanguage, + text, + targetLanguage, + }); this.translations.translate(this.appsState.appName, request) .subscribe(result => { diff --git a/frontend/src/app/features/content/shared/forms/content-section.component.ts b/frontend/src/app/features/content/shared/forms/content-section.component.ts index 9b8ddc269..bb369e792 100644 --- a/frontend/src/app/features/content/shared/forms/content-section.component.ts +++ b/frontend/src/app/features/content/shared/forms/content-section.component.ts @@ -7,7 +7,7 @@ import { AsyncPipe } from '@angular/common'; import { booleanAttribute, ChangeDetectionStrategy, Component, EventEmitter, forwardRef, Input, numberAttribute, Output } from '@angular/core'; -import { AppLanguageDto, EditContentForm, FieldForm, FieldSection, FormHintComponent, LocalStoreService, MarkdownDirective, RootFieldDto, SchemaDto, Settings, StatefulComponent, TypedSimpleChanges } from '@app/shared'; +import { AppLanguageDto, EditContentForm, FieldForm, FieldSection, FormHintComponent, LocalStoreService, MarkdownDirective, FieldDto, SchemaDto, Settings, StatefulComponent, TypedSimpleChanges } from '@app/shared'; import { ContentFieldComponent } from './content-field.component'; interface State { @@ -48,7 +48,7 @@ export class ContentSectionComponent extends StatefulComponent { public formContext!: any; @Input({ required: true }) - public formSection!: FieldSection; + public formSection!: FieldSection; @Input({ required: true }) public schema!: SchemaDto; diff --git a/frontend/src/app/features/content/shared/forms/field-editor.component.ts b/frontend/src/app/features/content/shared/forms/field-editor.component.ts index 2476967e3..bfbb2c89f 100644 --- a/frontend/src/app/features/content/shared/forms/field-editor.component.ts +++ b/frontend/src/app/features/content/shared/forms/field-editor.component.ts @@ -9,7 +9,7 @@ import { AsyncPipe } from '@angular/common'; import { booleanAttribute, Component, ElementRef, EventEmitter, Input, numberAttribute, Output, ViewChild } from '@angular/core'; import { AbstractControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { Observable } from 'rxjs'; -import { AbstractContentForm, AnnotationCreate, AnnotationsSelect, AppLanguageDto, ChatDialogComponent, CheckboxGroupComponent, CodeEditorComponent, ColorPickerComponent, CommentsState, ConfirmClickDirective, ControlErrorsComponent, DateTimeEditorComponent, DialogModel, disabled$, EditContentForm, FieldDto, FormHintComponent, GeolocationEditorComponent, hasNoValue$, HTTP, IndeterminateValueDirective, MarkdownDirective, MathHelper, MessageBus, ModalDirective, RadioGroupComponent, ReferenceInputComponent, RichEditorComponent, StarsComponent, TagEditorComponent, ToggleComponent, TooltipDirective, TransformInputDirective, TypedSimpleChanges, Types } from '@app/shared'; +import { AbstractContentForm, AnnotationCreate, AnnotationsSelect, AnyFieldDto, AppLanguageDto, ChatDialogComponent, CheckboxGroupComponent, CodeEditorComponent, ColorPickerComponent, CommentsState, ConfirmClickDirective, ControlErrorsComponent, DateTimeEditorComponent, DialogModel, disabled$, EditContentForm, FormHintComponent, GeolocationEditorComponent, hasNoValue$, HTTP, IndeterminateValueDirective, MarkdownDirective, MathHelper, MessageBus, ModalDirective, RadioGroupComponent, ReferenceInputComponent, RichEditorComponent, StarsComponent, TagEditorComponent, ToggleComponent, TooltipDirective, TransformInputDirective, TypedSimpleChanges, Types } from '@app/shared'; import { ReferenceDropdownComponent } from '../references/reference-dropdown.component'; import { ReferencesCheckboxesComponent } from '../references/references-checkboxes.component'; import { ReferencesEditorComponent } from '../references/references-editor.component'; @@ -84,7 +84,7 @@ export class FieldEditorComponent { public formLevel!: number; @Input({ required: true }) - public formModel!: AbstractContentForm; + public formModel!: AbstractContentForm; @Input({ required: true }) public language!: AppLanguageDto; @@ -142,17 +142,18 @@ export class FieldEditorComponent { public reset() { const editor = this.editor as any; + if (!editor) { + return; + } - if (editor) { - const nativeElement = this.editor.nativeElement; + const nativeElement = this.editor.nativeElement; - if (nativeElement && Types.isFunction(nativeElement['reset'])) { - nativeElement['reset'](); - } + if (nativeElement && Types.isFunction(nativeElement['reset'])) { + nativeElement['reset'](); + } - if (this.editor && Types.isFunction(editor['reset'])) { - editor['reset'](); - } + if (this.editor && Types.isFunction(editor['reset'])) { + editor['reset'](); } } diff --git a/frontend/src/app/features/content/shared/list/content.component.ts b/frontend/src/app/features/content/shared/list/content.component.ts index db819fe0e..62917baec 100644 --- a/frontend/src/app/features/content/shared/list/content.component.ts +++ b/frontend/src/app/features/content/shared/list/content.component.ts @@ -111,22 +111,23 @@ export class ContentComponent { } const value = this.patchForm.submit(); + if (!value) { + return; + } - if (value) { - this.contentsState.patch(this.content, value) - .subscribe({ - next: () => { - this.patchForm!.submitCompleted({ noReset: true }); + this.contentsState.patch(this.content, value) + .subscribe({ + next: () => { + this.patchForm!.submitCompleted({ noReset: true }); - this.changeDetector.detectChanges(); - }, - error: error => { - this.patchForm!.submitFailed(error); + this.changeDetector.detectChanges(); + }, + error: error => { + this.patchForm!.submitFailed(error); - this.changeDetector.detectChanges(); - }, - }); - } + this.changeDetector.detectChanges(); + }, + }); } public shouldStop(field: TableField) { diff --git a/frontend/src/app/features/content/shared/references/content-creator.component.ts b/frontend/src/app/features/content/shared/references/content-creator.component.ts index 6989d841e..d853cfc9c 100644 --- a/frontend/src/app/features/content/shared/references/content-creator.component.ts +++ b/frontend/src/app/features/content/shared/references/content-creator.component.ts @@ -106,26 +106,26 @@ export class ContentCreatorComponent implements OnInit { private saveContent(publish: boolean) { const value = this.contentForm.submit(); - - if (value) { - if (!this.canCreate(publish)) { - return; - } - - this.contentsState.create(value, publish) - .subscribe({ - next: content => { - this.contentForm.submitCompleted({ noReset: true }); - - this.emitSelect(content); - }, - error: error => { - this.contentForm.submitFailed(error); - }, - }); - } else { + if (!value) { this.contentForm.submitFailed('i18n:contents.contentNotValid'); + return; } + + if (!this.canCreate(publish)) { + return; + } + + this.contentsState.create(value, publish) + .subscribe({ + next: content => { + this.contentForm.submitCompleted({ noReset: true }); + + this.emitSelect(content); + }, + error: error => { + this.contentForm.submitFailed(error); + }, + }); } private canCreate(publish: boolean) { diff --git a/frontend/src/app/features/content/shared/references/reference-dropdown.component.ts b/frontend/src/app/features/content/shared/references/reference-dropdown.component.ts index 1e3b7acf0..45c1f16e6 100644 --- a/frontend/src/app/features/content/shared/references/reference-dropdown.component.ts +++ b/frontend/src/app/features/content/shared/references/reference-dropdown.component.ts @@ -8,7 +8,7 @@ import { booleanAttribute, ChangeDetectionStrategy, Component, forwardRef, Input } from '@angular/core'; import { FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule, UntypedFormControl } from '@angular/forms'; import { Observable } from 'rxjs'; -import { ContentDto, ContentsDto, DropdownComponent, getContentValue, HighlightPipe, LanguageDto, LocalizerService, ResolveContents, SafeHtmlPipe, StatefulControlComponent, Subscriptions, TypedSimpleChanges, Types, value$ } from '@app/shared'; +import { AppLanguageDto, ContentDto, ContentsDto, DropdownComponent, getContentValue, HighlightPipe, LocalizerService, ResolveContents, SafeHtmlPipe, StatefulControlComponent, Subscriptions, TypedSimpleChanges, Types, value$ } from '@app/shared'; export const SQX_REFERENCE_DROPDOWN_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ReferenceDropdownComponent), multi: true, @@ -53,10 +53,10 @@ export class ReferenceDropdownComponent extends StatefulControlComponent; + public languages!: ReadonlyArray; @Input({ required: true }) public schemaId!: string; diff --git a/frontend/src/app/features/content/shared/references/reference-item.component.ts b/frontend/src/app/features/content/shared/references/reference-item.component.ts index 93ea4129b..058f4830c 100644 --- a/frontend/src/app/features/content/shared/references/reference-item.component.ts +++ b/frontend/src/app/features/content/shared/references/reference-item.component.ts @@ -10,7 +10,7 @@ import { booleanAttribute, ChangeDetectionStrategy, Component, EventEmitter, Input, numberAttribute, Output } from '@angular/core'; import { RouterLink } from '@angular/router'; -import { ConfirmClickDirective, ContentDto, ContentListCellDirective, ContentListFieldComponent, ContentValueComponent, getContentValue, LanguageDto, META_FIELDS, TooltipDirective, TranslatePipe } from '@app/shared'; +import { AppLanguageDto, ConfirmClickDirective, ContentDto, ContentListCellDirective, ContentListFieldComponent, ContentValueComponent, getContentValue, META_FIELDS, TooltipDirective, TranslatePipe } from '@app/shared'; @Component({ standalone: true, @@ -38,10 +38,10 @@ export class ReferenceItemComponent { public clone = new EventEmitter(); @Input() - public language!: LanguageDto; + public language!: AppLanguageDto; @Input() - public languages!: ReadonlyArray; + public languages!: ReadonlyArray; @Input({ transform: booleanAttribute }) public canRemove?: boolean | null = true; diff --git a/frontend/src/app/features/content/shared/references/references-checkboxes.component.ts b/frontend/src/app/features/content/shared/references/references-checkboxes.component.ts index 62090a5cd..360f1580b 100644 --- a/frontend/src/app/features/content/shared/references/references-checkboxes.component.ts +++ b/frontend/src/app/features/content/shared/references/references-checkboxes.component.ts @@ -7,7 +7,7 @@ import { booleanAttribute, ChangeDetectionStrategy, Component, forwardRef, inject, Input } from '@angular/core'; import { FormControl, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms'; -import { AppsState, CheckboxGroupComponent, ContentDto, ContentsService, LanguageDto, LocalizerService, StatefulControlComponent, Subscriptions, TypedSimpleChanges, UIOptions } from '@app/shared'; +import { AppLanguageDto, AppsState, CheckboxGroupComponent, ContentDto, ContentsService, LocalizerService, StatefulControlComponent, Subscriptions, TypedSimpleChanges, UIOptions } from '@app/shared'; import { ReferencesTagsConverter } from './references-tag-converter'; export const SQX_REFERENCES_CHECKBOXES_CONTROL_VALUE_ACCESSOR: any = { @@ -45,7 +45,7 @@ export class ReferencesCheckboxesComponent extends StatefulControlComponent = []; - constructor(language: LanguageDto, contents: ReadonlyArray, + constructor(language: AppLanguageDto, contents: ReadonlyArray, private readonly localizer: LocalizerService, ) { this.tags = this.createTags(language, contents); @@ -28,7 +28,7 @@ export class ReferencesTagsConverter implements TagConverter { return result || null; } - private createTags(language: LanguageDto, contents: ReadonlyArray): ReadonlyArray { + private createTags(language: AppLanguageDto, contents: ReadonlyArray): ReadonlyArray { if (contents.length === 0) { return []; } diff --git a/frontend/src/app/features/content/shared/references/references-tags.component.ts b/frontend/src/app/features/content/shared/references/references-tags.component.ts index e097a95c9..b0a506d51 100644 --- a/frontend/src/app/features/content/shared/references/references-tags.component.ts +++ b/frontend/src/app/features/content/shared/references/references-tags.component.ts @@ -8,7 +8,7 @@ import { booleanAttribute, ChangeDetectionStrategy, Component, forwardRef, Input } from '@angular/core'; import { FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule, UntypedFormControl } from '@angular/forms'; import { Observable } from 'rxjs'; -import { ContentDto, ContentsDto, LanguageDto, LocalizerService, ResolveContents, StatefulControlComponent, Subscriptions, TagEditorComponent, TranslatePipe, TypedSimpleChanges, Types } from '@app/shared'; +import { AppLanguageDto, ContentDto, ContentsDto, LocalizerService, ResolveContents, StatefulControlComponent, Subscriptions, TagEditorComponent, TranslatePipe, TypedSimpleChanges, Types } from '@app/shared'; import { ReferencesTagsConverter } from './references-tag-converter'; export const SQX_REFERENCES_TAGS_CONTROL_VALUE_ACCESSOR: any = { @@ -51,10 +51,10 @@ export class ReferencesTagsComponent extends StatefulControlComponent; + public languages!: ReadonlyArray; @Input({ transform: booleanAttribute }) public set disabled(value: boolean | undefined | null) { diff --git a/frontend/src/app/features/rules/pages/rule/rule-page.component.html b/frontend/src/app/features/rules/pages/rule/rule-page.component.html index 314e47a40..99a1b10bc 100644 --- a/frontend/src/app/features/rules/pages/rule/rule-page.component.html +++ b/frontend/src/app/features/rules/pages/rule/rule-page.component.html @@ -101,7 +101,7 @@ [triggerForm]="currentTrigger"> } @case ("SchemaChanged") { - + } @case ("Usage") { @@ -162,7 +162,7 @@ } @else {
diff --git a/frontend/src/app/features/rules/pages/rule/rule-page.component.ts b/frontend/src/app/features/rules/pages/rule/rule-page.component.ts index 0adb33de5..7841998f8 100644 --- a/frontend/src/app/features/rules/pages/rule/rule-page.component.ts +++ b/frontend/src/app/features/rules/pages/rule/rule-page.component.ts @@ -10,7 +10,7 @@ import { Component, OnInit } from '@angular/core'; import { AbstractControl, FormsModule } from '@angular/forms'; import { ActivatedRoute, Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; import { debounceTime, Subscription } from 'rxjs'; -import { ActionForm, ALL_TRIGGERS, ConfirmClickDirective, FormAlertComponent, KeysPipe, LayoutComponent, ListViewComponent, MessageBus, RuleDto, RuleElementDto, RulesService, RulesState, SchemasState, SidebarMenuDirective, Subscriptions, TitleComponent, ToggleComponent, TooltipDirective, TourHintDirective, TourStepDirective, TranslatePipe, TriggerForm, TriggerType, value$ } from '@app/shared'; +import { ActionForm, ALL_TRIGGERS, ConfirmClickDirective, DynamicCreateRuleDto, DynamicRuleDto, DynamicUpdateRuleDto, FormAlertComponent, KeysPipe, LayoutComponent, ListViewComponent, MessageBus, RuleElementDto, RulesService, RulesState, SchemasState, SidebarMenuDirective, Subscriptions, TitleComponent, ToggleComponent, TooltipDirective, TourHintDirective, TourStepDirective, TranslatePipe, TriggerForm, value$ } from '@app/shared'; import { GenericActionComponent } from '../../shared/actions/generic-action.component'; import { RuleElementComponent } from '../../shared/rule-element.component'; import { AssetChangedTriggerComponent } from '../../shared/triggers/asset-changed-trigger.component'; @@ -60,7 +60,7 @@ export class RulePageComponent implements OnInit { public supportedTriggers = ALL_TRIGGERS; public supportedActions: { [name: string]: RuleElementDto } = {}; - public rule?: RuleDto | null; + public rule?: DynamicRuleDto | null; public currentTrigger?: TriggerForm; public currentAction?: ActionForm; @@ -69,7 +69,7 @@ export class RulePageComponent implements OnInit { public isEditable = false; public get isManual() { - return this.rule?.triggerType === 'Manual'; + return this.rule?.trigger.triggerType === 'Manual'; } public get actionElement() { @@ -77,7 +77,7 @@ export class RulePageComponent implements OnInit { } public get triggerElement() { - return this.supportedTriggers[(this.currentTrigger?.triggerType || '') as TriggerType]; + return this.supportedTriggers[this.currentTrigger!.triggerType]; } constructor( @@ -94,7 +94,6 @@ export class RulePageComponent implements OnInit { this.rulesService.getActions() .subscribe(actions => { this.supportedActions = actions; - this.initFromRule(); }); @@ -102,7 +101,6 @@ export class RulePageComponent implements OnInit { this.rulesState.selectedRule .subscribe(rule => { this.rule = rule; - this.initFromRule(); })); @@ -114,8 +112,8 @@ export class RulePageComponent implements OnInit { this.isEditable = this.rule.canUpdate; this.isEnabled = this.rule.isEnabled; - this.selectAction(this.rule.actionType, this.rule.action); - this.selectTrigger(this.rule.triggerType, this.rule.trigger); + this.selectAction(this.rule.action.actionType, this.rule.action); + this.selectTrigger(this.rule.trigger.triggerType, this.rule.trigger); } else { this.isEditable = true; this.isEnabled = false; @@ -140,7 +138,7 @@ export class RulePageComponent implements OnInit { } } - public selectTrigger(type: TriggerType, values?: any) { + public selectTrigger(type: string, values?: any) { if (this.currentTrigger?.triggerType !== type) { this.currentTrigger = new TriggerForm(type); this.currentTrigger.setEnabled(this.isEditable); @@ -175,20 +173,18 @@ export class RulePageComponent implements OnInit { } const action = this.currentAction.submit(); - if (!action) { return; } const trigger = this.currentTrigger.submit(); - if (!trigger || !action) { return; } - const request: any = { trigger, action, isEnabled: this.isEnabled }; - if (this.rule) { + const request = new DynamicUpdateRuleDto({ trigger, action, isEnabled: this.isEnabled }); + this.rulesState.update(this.rule, request) .subscribe({ next: () => { @@ -199,6 +195,8 @@ export class RulePageComponent implements OnInit { }, }); } else { + const request = new DynamicCreateRuleDto({ trigger, action }); + this.rulesState.create(request) .subscribe({ next: rule => { diff --git a/frontend/src/app/features/rules/pages/rules/rule.component.html b/frontend/src/app/features/rules/pages/rules/rule.component.html index 20af1d238..074da84d9 100644 --- a/frontend/src/app/features/rules/pages/rules/rule.component.html +++ b/frontend/src/app/features/rules/pages/rules/rule.component.html @@ -89,7 +89,7 @@

{{ "rules.ruleSyntax.if" | sqxTranslate }}

- +

{{ "rules.ruleSyntax.then" | sqxTranslate }}

@@ -97,8 +97,8 @@
+ [element]="$any(ruleActions[rule.action.actionType])" + [type]="rule.action.actionType">
@if (isManual) { diff --git a/frontend/src/app/features/rules/pages/rules/rule.component.ts b/frontend/src/app/features/rules/pages/rules/rule.component.ts index d1141f13a..2f4bea186 100644 --- a/frontend/src/app/features/rules/pages/rules/rule.component.ts +++ b/frontend/src/app/features/rules/pages/rules/rule.component.ts @@ -9,7 +9,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { RouterLink } from '@angular/router'; -import { ActionsDto, ConfirmClickDirective, DropdownMenuComponent, EditableTitleComponent, ModalDirective, ModalModel, ModalPlacementDirective, RuleDto, RulesState, ToggleComponent, TranslatePipe, TriggersDto } from '@app/shared'; +import { ActionsDto, ConfirmClickDirective, DropdownMenuComponent, DynamicRuleDto, DynamicUpdateRuleDto, EditableTitleComponent, ModalDirective, ModalModel, ModalPlacementDirective, RulesState, ToggleComponent, TranslatePipe, TriggersDto } from '@app/shared'; import { RuleElementComponent } from '../../shared/rule-element.component'; @Component({ @@ -39,12 +39,12 @@ export class RuleComponent { public ruleActions!: ActionsDto; @Input({ required: true }) - public rule!: RuleDto; + public rule!: DynamicRuleDto; public dropdown = new ModalModel(); public get isManual() { - return this.rule.triggerType === 'Manual'; + return this.rule.trigger.triggerType === 'Manual'; } constructor( @@ -65,19 +65,19 @@ export class RuleComponent { } public rename(name: string) { - this.rulesState.update(this.rule, { name }); + this.rulesState.update(this.rule, new DynamicUpdateRuleDto({ name })); } public disable() { - this.rulesState.update(this.rule, { isEnabled: false }); + this.rulesState.update(this.rule, new DynamicUpdateRuleDto({ isEnabled: false })); } public enable() { - this.rulesState.update(this.rule, { isEnabled: true }); + this.rulesState.update(this.rule, new DynamicUpdateRuleDto({ isEnabled: true })); } public toggle() { - this.rulesState.update(this.rule, { isEnabled: !this.rule.isEnabled }); + this.rulesState.update(this.rule, new DynamicUpdateRuleDto({ isEnabled: !this.rule.isEnabled })); } public trigger() { diff --git a/frontend/src/app/features/rules/pages/rules/rules-page.component.ts b/frontend/src/app/features/rules/pages/rules/rules-page.component.ts index 99fcefb62..89d7c186c 100644 --- a/frontend/src/app/features/rules/pages/rules/rules-page.component.ts +++ b/frontend/src/app/features/rules/pages/rules/rules-page.component.ts @@ -8,7 +8,7 @@ import { AsyncPipe } from '@angular/common'; import { Component, OnInit } from '@angular/core'; import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; -import { ALL_TRIGGERS, LayoutComponent, ListViewComponent, RuleDto, RuleElementDto, RulesService, RulesState, SchemasState, ShortcutDirective, SidebarMenuDirective, TitleComponent, TooltipDirective, TourHintDirective, TourStepDirective, TranslatePipe } from '@app/shared'; +import { ALL_TRIGGERS, DynamicRuleDto, DynamicUpdateRuleDto, LayoutComponent, ListViewComponent, RuleElementDto, RulesService, RulesState, SchemasState, ShortcutDirective, SidebarMenuDirective, TitleComponent, TooltipDirective, TourHintDirective, TourStepDirective, TranslatePipe } from '@app/shared'; import { RuleComponent } from './rule.component'; @Component({ @@ -63,11 +63,11 @@ export class RulesPageComponent implements OnInit { this.rulesState.runCancel(); } - public delete(rule: RuleDto) { + public delete(rule: DynamicRuleDto) { this.rulesState.delete(rule); } - public toggle(rule: RuleDto) { - this.rulesState.update(rule, { isEnabled: !rule.isEnabled }); + public toggle(rule: DynamicRuleDto) { + this.rulesState.update(rule, new DynamicUpdateRuleDto({ isEnabled: !rule.isEnabled })); } } diff --git a/frontend/src/app/features/rules/shared/actions/generic-action.component.html b/frontend/src/app/features/rules/shared/actions/generic-action.component.html index de0405946..36550be3a 100644 --- a/frontend/src/app/features/rules/shared/actions/generic-action.component.html +++ b/frontend/src/app/features/rules/shared/actions/generic-action.component.html @@ -31,7 +31,7 @@ } } - @case ("JavaScript") { + @case ("Javascript") { { - this.fieldForm.submitCompleted({ noReset: true }); - }, - error: error => { - this.fieldForm.submitFailed(error); - }, - }); + if (!value) { + return; } + + this.schemasState.update(this.schema, value) + .subscribe({ + next: () => { + this.fieldForm.submitCompleted({ noReset: true }); + }, + error: error => { + this.fieldForm.submitFailed(error); + }, + }); } } diff --git a/frontend/src/app/features/schemas/pages/schema/export/schema-export-form.component.ts b/frontend/src/app/features/schemas/pages/schema/export/schema-export-form.component.ts index 7f34d0dab..cf8f70a87 100644 --- a/frontend/src/app/features/schemas/pages/schema/export/schema-export-form.component.ts +++ b/frontend/src/app/features/schemas/pages/schema/export/schema-export-form.component.ts @@ -47,17 +47,18 @@ export class SchemaExportFormComponent { } const value = this.synchronizeForm.submit(); - - if (value) { - this.schemasState.synchronize(this.schema, value) - .subscribe({ - next: () => { - this.synchronizeForm.submitCompleted({ noReset: true }); - }, - error: error => { - this.synchronizeForm.submitFailed(error); - }, - }); + if (!value) { + return; } + + this.schemasState.synchronize(this.schema, value) + .subscribe({ + next: () => { + this.synchronizeForm.submitCompleted({ noReset: true }); + }, + error: error => { + this.synchronizeForm.submitFailed(error); + }, + }); } } diff --git a/frontend/src/app/features/schemas/pages/schema/fields/field-group.component.ts b/frontend/src/app/features/schemas/pages/schema/fields/field-group.component.ts index 3b32b1ab6..c954716bf 100644 --- a/frontend/src/app/features/schemas/pages/schema/fields/field-group.component.ts +++ b/frontend/src/app/features/schemas/pages/schema/fields/field-group.component.ts @@ -8,7 +8,7 @@ import { CdkDrag, CdkDragDrop, CdkDragHandle, CdkDropList } from '@angular/cdk/drag-drop'; import { booleanAttribute, Component, EventEmitter, Input, Output } from '@angular/core'; -import { AppSettingsDto, FieldDto, FieldGroup, LanguageDto, LocalStoreService, RootFieldDto, SchemaDto, Settings, StatefulComponent } from '@app/shared'; +import { AppLanguageDto, AppSettingsDto, FieldDto, FieldGroup, LocalStoreService, SchemaDto, Settings, StatefulComponent } from '@app/shared'; import { FieldComponent } from './field.component'; interface State { @@ -33,10 +33,10 @@ export class FieldGroupComponent extends StatefulComponent { public sorted = new EventEmitter>(); @Input({ required: true }) - public languages!: ReadonlyArray; + public languages!: ReadonlyArray; @Input() - public parent?: RootFieldDto; + public parent?: FieldDto; @Input({ required: true }) public settings!: AppSettingsDto; @@ -56,7 +56,7 @@ export class FieldGroupComponent extends StatefulComponent { public trackByFieldFn: (_index: number, field: FieldDto) => any; public get hasAnyFields() { - return this.parent ? this.parent.nested.length > 0 : this.schema.fields.length > 0; + return this.parent ? this.parent.nested && this.parent.nested.length > 0 : this.schema.fields.length > 0; } constructor( diff --git a/frontend/src/app/features/schemas/pages/schema/fields/field-wizard.component.html b/frontend/src/app/features/schemas/pages/schema/fields/field-wizard.component.html index 1526d47a4..18b54943e 100644 --- a/frontend/src/app/features/schemas/pages/schema/fields/field-wizard.component.html +++ b/frontend/src/app/features/schemas/pages/schema/fields/field-wizard.component.html @@ -11,7 +11,7 @@ @if (editForm) {
{ switch (navigationMode) { diff --git a/frontend/src/app/features/schemas/pages/schema/fields/field.component.ts b/frontend/src/app/features/schemas/pages/schema/fields/field.component.ts index 501483bf1..708b42b5b 100644 --- a/frontend/src/app/features/schemas/pages/schema/fields/field.component.ts +++ b/frontend/src/app/features/schemas/pages/schema/fields/field.component.ts @@ -8,7 +8,7 @@ import { booleanAttribute, Component, forwardRef, Input } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { AppSettingsDto, ConfirmClickDirective, createProperties, DialogModel, DropdownMenuComponent, EditFieldForm, FieldDto, LanguageDto, ModalDirective, ModalModel, ModalPlacementDirective, NestedFieldDto, RootFieldDto, SchemaDto, SchemasState, TooltipDirective, TourStepDirective, TranslatePipe, TypedSimpleChanges } from '@app/shared'; +import { AppLanguageDto, AppSettingsDto, ConfirmClickDirective, createProperties, DialogModel, DropdownMenuComponent, EditFieldForm, FieldDto, ModalDirective, ModalModel, ModalPlacementDirective, SchemaDto, SchemasState, TooltipDirective, TourStepDirective, TranslatePipe, TypedSimpleChanges, UpdateFieldDto } from '@app/shared'; import { FieldWizardComponent } from './field-wizard.component'; import { FieldFormComponent } from './forms/field-form.component'; import { SortableFieldListComponent } from './sortable-field-list.component'; @@ -35,7 +35,7 @@ import { SortableFieldListComponent } from './sortable-field-list.component'; }) export class FieldComponent { @Input({ required: true }) - public field!: NestedFieldDto | RootFieldDto; + public field!: FieldDto | FieldDto; @Input({ required: true }) public schema!: SchemaDto; @@ -44,10 +44,10 @@ export class FieldComponent { public plain = false; @Input() - public parent?: RootFieldDto; + public parent?: FieldDto; @Input({ required: true }) - public languages!: ReadonlyArray; + public languages!: ReadonlyArray; @Input({ required: true }) public settings!: AppSettingsDto; @@ -118,19 +118,20 @@ export class FieldComponent { } const value = this.editForm.submit(); - - if (value) { - const properties = createProperties(this.field.properties.fieldType, value); - - this.schemasState.updateField(this.schema, this.field, { properties }) - .subscribe({ - next: () => { - this.editForm.submitCompleted({ noReset: true }); - }, - error: error => { - this.editForm.submitFailed(error); - }, - }); + if (!value) { + return; } + + const properties = createProperties(this.field.properties.fieldType as any, value); + + this.schemasState.updateField(this.schema, this.field, new UpdateFieldDto({ properties })) + .subscribe({ + next: () => { + this.editForm.submitCompleted({ noReset: true }); + }, + error: error => { + this.editForm.submitFailed(error); + }, + }); } } diff --git a/frontend/src/app/features/schemas/pages/schema/fields/forms/field-form-validation.component.ts b/frontend/src/app/features/schemas/pages/schema/fields/forms/field-form-validation.component.ts index c61b9822d..84e5128c6 100644 --- a/frontend/src/app/features/schemas/pages/schema/fields/forms/field-form-validation.component.ts +++ b/frontend/src/app/features/schemas/pages/schema/fields/forms/field-form-validation.component.ts @@ -8,7 +8,7 @@ import { booleanAttribute, Component, Input } from '@angular/core'; import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; -import { AppSettingsDto, FieldDto, LanguageDto, SchemaDto, TranslatePipe } from '@app/shared'; +import { AppLanguageDto, AppSettingsDto, FieldDto, SchemaDto, TranslatePipe } from '@app/shared'; import { ArrayValidationComponent } from '../types/array-validation.component'; import { AssetsValidationComponent } from '../types/assets-validation.component'; import { BooleanValidationComponent } from '../types/boolean-validation.component'; @@ -61,7 +61,7 @@ export class FieldFormValidationComponent { public settings!: AppSettingsDto; @Input({ required: true }) - public languages!: ReadonlyArray; + public languages!: ReadonlyArray; @Input({ transform: booleanAttribute }) public isLocalizable?: boolean | null; diff --git a/frontend/src/app/features/schemas/pages/schema/fields/forms/field-form.component.ts b/frontend/src/app/features/schemas/pages/schema/fields/forms/field-form.component.ts index c5643f30c..db7fc8bb2 100644 --- a/frontend/src/app/features/schemas/pages/schema/fields/forms/field-form.component.ts +++ b/frontend/src/app/features/schemas/pages/schema/fields/forms/field-form.component.ts @@ -8,7 +8,7 @@ import { AfterViewInit, booleanAttribute, Component, EventEmitter, Input, Output } from '@angular/core'; import { UntypedFormGroup } from '@angular/forms'; -import { AppSettingsDto, FieldDto, LanguageDto, SchemaDto, TranslatePipe } from '@app/shared'; +import { AppLanguageDto, AppSettingsDto, FieldDto, SchemaDto, TranslatePipe } from '@app/shared'; import { JsonMoreComponent } from '../types/json-more.component'; import { FieldFormCommonComponent } from './field-form-common.component'; import { FieldFormUIComponent } from './field-form-ui.component'; @@ -47,7 +47,7 @@ export class FieldFormComponent implements AfterViewInit { public settings!: AppSettingsDto; @Input({ required: true }) - public languages!: ReadonlyArray; + public languages!: ReadonlyArray; @Input({ transform: booleanAttribute }) public isLocalizable?: boolean | null; diff --git a/frontend/src/app/features/schemas/pages/schema/fields/sortable-field-list.component.ts b/frontend/src/app/features/schemas/pages/schema/fields/sortable-field-list.component.ts index 8489a7889..166e50915 100644 --- a/frontend/src/app/features/schemas/pages/schema/fields/sortable-field-list.component.ts +++ b/frontend/src/app/features/schemas/pages/schema/fields/sortable-field-list.component.ts @@ -8,7 +8,7 @@ import { CdkDrag, CdkDragDrop, CdkDragHandle, CdkDropList, CdkDropListGroup, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop'; import { booleanAttribute, Component, EventEmitter, forwardRef, Input, Output } from '@angular/core'; -import { AppSettingsDto, FieldDto, FieldGroup, groupFields, LanguageDto, RootFieldDto, SchemaDto } from '@app/shared'; +import { AppLanguageDto, AppSettingsDto, FieldDto, FieldGroup, groupFields, SchemaDto } from '@app/shared'; import { FieldGroupComponent } from './field-group.component'; @Component({ @@ -29,10 +29,10 @@ export class SortableFieldListComponent { public sorted = new EventEmitter>(); @Input({ required: true }) - public languages!: ReadonlyArray; + public languages!: ReadonlyArray; @Input() - public parent?: RootFieldDto; + public parent?: FieldDto; @Input({ required: true }) public settings!: AppSettingsDto; diff --git a/frontend/src/app/features/schemas/pages/schema/fields/types/assets-validation.component.ts b/frontend/src/app/features/schemas/pages/schema/fields/types/assets-validation.component.ts index 6c27cfdba..fb38c5bc3 100644 --- a/frontend/src/app/features/schemas/pages/schema/fields/types/assets-validation.component.ts +++ b/frontend/src/app/features/schemas/pages/schema/fields/types/assets-validation.component.ts @@ -8,7 +8,7 @@ import { booleanAttribute, Component, Input } from '@angular/core'; import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; -import { AssetsFieldPropertiesDto, FieldDto, FormHintComponent, LanguageDto, LocalizedInputComponent, TagEditorComponent, TranslatePipe } from '@app/shared'; +import { AppLanguageDto, AssetsFieldPropertiesDto, FieldDto, FormHintComponent, LocalizedInputComponent, TagEditorComponent, TranslatePipe } from '@app/shared'; @Component({ standalone: true, @@ -35,7 +35,7 @@ export class AssetsValidationComponent { public properties!: AssetsFieldPropertiesDto; @Input({ required: true }) - public languages!: ReadonlyArray; + public languages!: ReadonlyArray; @Input({ transform: booleanAttribute }) public isLocalizable?: boolean | null; diff --git a/frontend/src/app/features/schemas/pages/schema/fields/types/boolean-ui.component.ts b/frontend/src/app/features/schemas/pages/schema/fields/types/boolean-ui.component.ts index 4f50dec47..1d5883001 100644 --- a/frontend/src/app/features/schemas/pages/schema/fields/types/boolean-ui.component.ts +++ b/frontend/src/app/features/schemas/pages/schema/fields/types/boolean-ui.component.ts @@ -8,7 +8,7 @@ import { Component, Input } from '@angular/core'; import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; -import { BOOLEAN_FIELD_EDITORS, BooleanFieldPropertiesDto, FieldDto, FormHintComponent, TranslatePipe } from '@app/shared'; +import { BooleanFieldEditorValues, BooleanFieldPropertiesDto, FieldDto, FormHintComponent, TranslatePipe } from '@app/shared'; @Component({ standalone: true, @@ -23,7 +23,7 @@ import { BOOLEAN_FIELD_EDITORS, BooleanFieldPropertiesDto, FieldDto, FormHintCom ], }) export class BooleanUIComponent { - public readonly editors = BOOLEAN_FIELD_EDITORS; + public readonly editors = BooleanFieldEditorValues; @Input({ required: true }) public fieldForm!: UntypedFormGroup; diff --git a/frontend/src/app/features/schemas/pages/schema/fields/types/boolean-validation.component.ts b/frontend/src/app/features/schemas/pages/schema/fields/types/boolean-validation.component.ts index 127c5c167..3980ac22e 100644 --- a/frontend/src/app/features/schemas/pages/schema/fields/types/boolean-validation.component.ts +++ b/frontend/src/app/features/schemas/pages/schema/fields/types/boolean-validation.component.ts @@ -9,7 +9,7 @@ import { booleanAttribute, Component, Input } from '@angular/core'; import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; import { Observable } from 'rxjs'; -import { BooleanFieldPropertiesDto, FieldDto, FormHintComponent, hasNoValue$, IndeterminateValueDirective, LanguageDto, LocalizedInputComponent, TranslatePipe, TypedSimpleChanges } from '@app/shared'; +import { AppLanguageDto, BooleanFieldPropertiesDto, FieldDto, FormHintComponent, hasNoValue$, IndeterminateValueDirective, LocalizedInputComponent, TranslatePipe, TypedSimpleChanges } from '@app/shared'; @Component({ standalone: true, @@ -36,7 +36,7 @@ export class BooleanValidationComponent { public properties!: BooleanFieldPropertiesDto; @Input({ required: true }) - public languages!: ReadonlyArray; + public languages!: ReadonlyArray; @Input({ transform: booleanAttribute }) public isLocalizable?: boolean | null; diff --git a/frontend/src/app/features/schemas/pages/schema/fields/types/date-time-ui.component.ts b/frontend/src/app/features/schemas/pages/schema/fields/types/date-time-ui.component.ts index e4c728dc4..04d18b33e 100644 --- a/frontend/src/app/features/schemas/pages/schema/fields/types/date-time-ui.component.ts +++ b/frontend/src/app/features/schemas/pages/schema/fields/types/date-time-ui.component.ts @@ -8,7 +8,7 @@ import { Component, Input } from '@angular/core'; import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; -import { DATETIME_FIELD_EDITORS, DateTimeFieldPropertiesDto, FieldDto, FloatConverter, FormHintComponent, MarkdownDirective, TranslatePipe } from '@app/shared'; +import { DateTimeFieldEditorValues, DateTimeFieldPropertiesDto, FieldDto, FloatConverter, FormHintComponent, MarkdownDirective, TranslatePipe } from '@app/shared'; @Component({ standalone: true, @@ -25,7 +25,7 @@ import { DATETIME_FIELD_EDITORS, DateTimeFieldPropertiesDto, FieldDto, FloatConv }) export class DateTimeUIComponent { public readonly converter = FloatConverter.INSTANCE; - public readonly editors = DATETIME_FIELD_EDITORS; + public readonly editors = DateTimeFieldEditorValues; @Input({ required: true }) public fieldForm!: UntypedFormGroup; diff --git a/frontend/src/app/features/schemas/pages/schema/fields/types/date-time-validation.component.ts b/frontend/src/app/features/schemas/pages/schema/fields/types/date-time-validation.component.ts index 85c7af7c4..c34f5e969 100644 --- a/frontend/src/app/features/schemas/pages/schema/fields/types/date-time-validation.component.ts +++ b/frontend/src/app/features/schemas/pages/schema/fields/types/date-time-validation.component.ts @@ -9,7 +9,7 @@ import { AsyncPipe } from '@angular/common'; import { booleanAttribute, Component, Input } from '@angular/core'; import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; import { Observable } from 'rxjs'; -import { DateTimeEditorComponent, DateTimeFieldPropertiesDto, FieldDto, FormHintComponent, hasNoValue$, LanguageDto, LocalizedInputComponent, TranslatePipe, TypedSimpleChanges } from '@app/shared'; +import { AppLanguageDto, DateTimeEditorComponent, DateTimeFieldPropertiesDto, FieldDto, FormHintComponent, hasNoValue$, LocalizedInputComponent, TranslatePipe, TypedSimpleChanges } from '@app/shared'; const CALCULATED_DEFAULT_VALUES: ReadonlyArray = ['Now', 'Today']; @@ -39,7 +39,7 @@ export class DateTimeValidationComponent { public properties!: DateTimeFieldPropertiesDto; @Input({ required: true }) - public languages!: ReadonlyArray; + public languages!: ReadonlyArray; @Input({ transform: booleanAttribute }) public isLocalizable?: boolean | null; diff --git a/frontend/src/app/features/schemas/pages/schema/fields/types/number-ui.component.ts b/frontend/src/app/features/schemas/pages/schema/fields/types/number-ui.component.ts index 97e7b29ea..a0c4ae2c8 100644 --- a/frontend/src/app/features/schemas/pages/schema/fields/types/number-ui.component.ts +++ b/frontend/src/app/features/schemas/pages/schema/fields/types/number-ui.component.ts @@ -9,7 +9,7 @@ import { AsyncPipe } from '@angular/common'; import { Component, Input } from '@angular/core'; import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; import { Observable } from 'rxjs'; -import { FieldDto, FloatConverter, FormHintComponent, NUMBER_FIELD_EDITORS, NumberFieldPropertiesDto, Subscriptions, TagEditorComponent, TranslatePipe, TypedSimpleChanges, valueProjection$ } from '@app/shared'; +import { FieldDto, FloatConverter, FormHintComponent, NumberFieldEditorValues, NumberFieldPropertiesDto, Subscriptions, TagEditorComponent, TranslatePipe, TypedSimpleChanges, valueProjection$ } from '@app/shared'; @Component({ standalone: true, @@ -29,7 +29,7 @@ export class NumberUIComponent { private readonly subscriptions = new Subscriptions(); public readonly converter = FloatConverter.INSTANCE; - public readonly editors = NUMBER_FIELD_EDITORS; + public readonly editors = NumberFieldEditorValues; @Input({ required: true }) public fieldForm!: UntypedFormGroup; diff --git a/frontend/src/app/features/schemas/pages/schema/fields/types/number-validation.component.ts b/frontend/src/app/features/schemas/pages/schema/fields/types/number-validation.component.ts index 9427d1c9f..ae5e361b4 100644 --- a/frontend/src/app/features/schemas/pages/schema/fields/types/number-validation.component.ts +++ b/frontend/src/app/features/schemas/pages/schema/fields/types/number-validation.component.ts @@ -8,7 +8,7 @@ import { booleanAttribute, Component, Input } from '@angular/core'; import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; -import { FieldDto, FormHintComponent, LanguageDto, LocalizedInputComponent, NumberFieldPropertiesDto, RootFieldDto, SchemaDto, TranslatePipe, Types } from '@app/shared'; +import { AppLanguageDto, FieldDto, FormHintComponent, LocalizedInputComponent, NumberFieldPropertiesDto, SchemaDto, TranslatePipe, Types } from '@app/shared'; @Component({ standalone: true, @@ -37,12 +37,12 @@ export class NumberValidationComponent { public properties!: NumberFieldPropertiesDto; @Input({ required: true }) - public languages!: ReadonlyArray; + public languages!: ReadonlyArray; @Input({ transform: booleanAttribute }) public isLocalizable?: boolean | null; public get showUnique() { - return Types.is(this.field, RootFieldDto) && !this.field.isLocalizable && this.schema.type !== 'Component'; + return Types.is(this.field, FieldDto) && !this.field.isLocalizable && this.schema.type !== 'Component'; } } diff --git a/frontend/src/app/features/schemas/pages/schema/fields/types/references-ui.component.ts b/frontend/src/app/features/schemas/pages/schema/fields/types/references-ui.component.ts index 5c93f3cba..b2f82021a 100644 --- a/frontend/src/app/features/schemas/pages/schema/fields/types/references-ui.component.ts +++ b/frontend/src/app/features/schemas/pages/schema/fields/types/references-ui.component.ts @@ -8,7 +8,7 @@ import { Component, Input } from '@angular/core'; import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; -import { FieldDto, FormHintComponent, REFERENCES_FIELD_EDITORS, ReferencesFieldPropertiesDto, TranslatePipe } from '@app/shared'; +import { FieldDto, FormHintComponent, ReferencesFieldEditorValues, ReferencesFieldPropertiesDto, TranslatePipe } from '@app/shared'; @Component({ standalone: true, @@ -23,7 +23,7 @@ import { FieldDto, FormHintComponent, REFERENCES_FIELD_EDITORS, ReferencesFieldP ], }) export class ReferencesUIComponent { - public readonly editors = REFERENCES_FIELD_EDITORS; + public readonly editors = ReferencesFieldEditorValues; @Input({ required: true }) public fieldForm!: UntypedFormGroup; diff --git a/frontend/src/app/features/schemas/pages/schema/fields/types/references-validation.component.ts b/frontend/src/app/features/schemas/pages/schema/fields/types/references-validation.component.ts index 8351030e2..6a37d8b76 100644 --- a/frontend/src/app/features/schemas/pages/schema/fields/types/references-validation.component.ts +++ b/frontend/src/app/features/schemas/pages/schema/fields/types/references-validation.component.ts @@ -8,7 +8,7 @@ import { AsyncPipe } from '@angular/common'; import { booleanAttribute, Component, Input } from '@angular/core'; import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; -import { FieldDto, FormHintComponent, LanguageDto, LocalizedInputComponent, ReferencesFieldPropertiesDto, SchemaTagSource, TagEditorComponent, TranslatePipe } from '@app/shared'; +import { AppLanguageDto, FieldDto, FormHintComponent, LocalizedInputComponent, ReferencesFieldPropertiesDto, SchemaTagSource, TagEditorComponent, TranslatePipe } from '@app/shared'; @Component({ standalone: true, @@ -36,7 +36,7 @@ export class ReferencesValidationComponent { public properties!: ReferencesFieldPropertiesDto; @Input({ required: true }) - public languages!: ReadonlyArray; + public languages!: ReadonlyArray; @Input({ transform: booleanAttribute }) public isLocalizable?: boolean | null; diff --git a/frontend/src/app/features/schemas/pages/schema/fields/types/string-ui.component.ts b/frontend/src/app/features/schemas/pages/schema/fields/types/string-ui.component.ts index 80f6a3e5e..1e1a815db 100644 --- a/frontend/src/app/features/schemas/pages/schema/fields/types/string-ui.component.ts +++ b/frontend/src/app/features/schemas/pages/schema/fields/types/string-ui.component.ts @@ -9,7 +9,7 @@ import { AsyncPipe } from '@angular/common'; import { Component, Input } from '@angular/core'; import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; import { Observable } from 'rxjs'; -import { AssetFolderDropdownComponent, FieldDto, FormHintComponent, SchemaTagSource, STRING_FIELD_EDITORS, StringFieldPropertiesDto, Subscriptions, TagEditorComponent, TranslatePipe, TypedSimpleChanges, valueProjection$ } from '@app/shared'; +import { AssetFolderDropdownComponent, FieldDto, FormHintComponent, SchemaTagSource, StringFieldEditorValues, StringFieldPropertiesDto, Subscriptions, TagEditorComponent, TranslatePipe, TypedSimpleChanges, valueProjection$ } from '@app/shared'; @Component({ standalone: true, @@ -29,7 +29,7 @@ import { AssetFolderDropdownComponent, FieldDto, FormHintComponent, SchemaTagSou export class StringUIComponent { private readonly subscriptions = new Subscriptions(); - public readonly editors = STRING_FIELD_EDITORS; + public readonly editors = StringFieldEditorValues; @Input({ required: true }) public fieldForm!: UntypedFormGroup; diff --git a/frontend/src/app/features/schemas/pages/schema/fields/types/string-validation.component.ts b/frontend/src/app/features/schemas/pages/schema/fields/types/string-validation.component.ts index b132293fe..cd00e149f 100644 --- a/frontend/src/app/features/schemas/pages/schema/fields/types/string-validation.component.ts +++ b/frontend/src/app/features/schemas/pages/schema/fields/types/string-validation.component.ts @@ -9,7 +9,7 @@ import { AsyncPipe } from '@angular/common'; import { booleanAttribute, Component, Input } from '@angular/core'; import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; import { Observable } from 'rxjs'; -import { AppSettingsDto, DropdownMenuComponent, FieldDto, FormHintComponent, hasNoValue$, hasValue$, LanguageDto, LocalizedInputComponent, ModalDirective, ModalModel, ModalPlacementDirective, PatternDto, RootFieldDto, SchemaDto, STRING_CONTENT_TYPES, StringFieldPropertiesDto, Subscriptions, TranslatePipe, TypedSimpleChanges, Types, value$ } from '@app/shared'; +import { AppLanguageDto, AppSettingsDto, DropdownMenuComponent, FieldDto, FormHintComponent, hasNoValue$, hasValue$, LocalizedInputComponent, ModalDirective, ModalModel, ModalPlacementDirective, PatternDto, SchemaDto, StringContentTypeValues, StringFieldPropertiesDto, Subscriptions, TranslatePipe, TypedSimpleChanges, Types, value$ } from '@app/shared'; @Component({ standalone: true, @@ -31,7 +31,7 @@ import { AppSettingsDto, DropdownMenuComponent, FieldDto, FormHintComponent, has export class StringValidationComponent { private readonly subscriptions = new Subscriptions(); - public readonly contentTypes = STRING_CONTENT_TYPES; + public readonly contentTypes = StringContentTypeValues; @Input({ required: true }) public fieldForm!: UntypedFormGroup; @@ -49,7 +49,7 @@ export class StringValidationComponent { public settings!: AppSettingsDto; @Input({ required: true }) - public languages!: ReadonlyArray; + public languages!: ReadonlyArray; @Input({ transform: booleanAttribute }) public isLocalizable?: boolean | null; @@ -61,7 +61,7 @@ export class StringValidationComponent { public patternsModal = new ModalModel(); public get showUnique() { - return Types.is(this.field, RootFieldDto) && !this.field.isLocalizable && this.schema.type !== 'Component'; + return Types.is(this.field, FieldDto) && !this.field.isLocalizable && this.schema.type !== 'Component'; } public ngOnChanges(changes: TypedSimpleChanges) { diff --git a/frontend/src/app/features/schemas/pages/schema/fields/types/tags-ui.component.ts b/frontend/src/app/features/schemas/pages/schema/fields/types/tags-ui.component.ts index 0d353c53d..e8d5ad1cd 100644 --- a/frontend/src/app/features/schemas/pages/schema/fields/types/tags-ui.component.ts +++ b/frontend/src/app/features/schemas/pages/schema/fields/types/tags-ui.component.ts @@ -8,7 +8,7 @@ import { Component, Input } from '@angular/core'; import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; -import { FieldDto, FormHintComponent, TagEditorComponent, TAGS_FIELD_EDITORS, TagsFieldPropertiesDto, TranslatePipe } from '@app/shared'; +import { FieldDto, FormHintComponent, TagEditorComponent, TagsFieldEditorValues, TagsFieldPropertiesDto, TranslatePipe } from '@app/shared'; @Component({ standalone: true, @@ -24,7 +24,7 @@ import { FieldDto, FormHintComponent, TagEditorComponent, TAGS_FIELD_EDITORS, Ta ], }) export class TagsUIComponent { - public readonly editors = TAGS_FIELD_EDITORS; + public readonly editors = TagsFieldEditorValues; @Input({ required: true }) public fieldForm!: UntypedFormGroup; diff --git a/frontend/src/app/features/schemas/pages/schema/fields/types/tags-validation.component.ts b/frontend/src/app/features/schemas/pages/schema/fields/types/tags-validation.component.ts index d6e76009b..92bf03c7d 100644 --- a/frontend/src/app/features/schemas/pages/schema/fields/types/tags-validation.component.ts +++ b/frontend/src/app/features/schemas/pages/schema/fields/types/tags-validation.component.ts @@ -8,7 +8,7 @@ import { booleanAttribute, Component, Input } from '@angular/core'; import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; -import { FieldDto, FormHintComponent, LanguageDto, LocalizedInputComponent, TagEditorComponent, TagsFieldPropertiesDto, TranslatePipe } from '@app/shared'; +import { AppLanguageDto, FieldDto, FormHintComponent, LocalizedInputComponent, TagEditorComponent, TagsFieldPropertiesDto, TranslatePipe } from '@app/shared'; @Component({ standalone: true, @@ -35,7 +35,7 @@ export class TagsValidationComponent { public properties!: TagsFieldPropertiesDto; @Input({ required: true }) - public languages!: ReadonlyArray; + public languages!: ReadonlyArray; @Input({ transform: booleanAttribute }) public isLocalizable?: boolean | null; diff --git a/frontend/src/app/features/schemas/pages/schema/indexes/index-form.component.ts b/frontend/src/app/features/schemas/pages/schema/indexes/index-form.component.ts index c3fdcb74b..efb84f391 100644 --- a/frontend/src/app/features/schemas/pages/schema/indexes/index-form.component.ts +++ b/frontend/src/app/features/schemas/pages/schema/indexes/index-form.component.ts @@ -83,18 +83,19 @@ export class IndexFormComponent { } public createSchema() { - const fields = this.createForm.submit(); - - if (fields) { - this.indexesState.create({ fields }) - .subscribe({ - next: () => { - this.emitCreate(); - }, - error: error => { - this.createForm.submitFailed(error); - }, - }); + const value = this.createForm.submit(); + if (!value) { + return; } + + this.indexesState.create(value) + .subscribe({ + next: () => { + this.emitCreate(); + }, + error: error => { + this.createForm.submitFailed(error); + }, + }); } } \ No newline at end of file diff --git a/frontend/src/app/features/schemas/pages/schema/preview/schema-preview-urls-form.component.ts b/frontend/src/app/features/schemas/pages/schema/preview/schema-preview-urls-form.component.ts index 25fc85d19..5f1df5b62 100644 --- a/frontend/src/app/features/schemas/pages/schema/preview/schema-preview-urls-form.component.ts +++ b/frontend/src/app/features/schemas/pages/schema/preview/schema-preview-urls-form.component.ts @@ -60,17 +60,18 @@ export class SchemaPreviewUrlsFormComponent implements OnInit { } const value = this.editForm.submit(); - - if (value) { - this.schemasState.configurePreviewUrls(this.schema, value) - .subscribe({ - next: () => { - this.editForm.submitCompleted({ noReset: true }); - }, - error: error => { - this.editForm.submitFailed(error); - }, - }); + if (!value) { + return; } + + this.schemasState.configurePreviewUrls(this.schema, value) + .subscribe({ + next: () => { + this.editForm.submitCompleted({ noReset: true }); + }, + error: error => { + this.editForm.submitFailed(error); + }, + }); } } diff --git a/frontend/src/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.ts b/frontend/src/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.ts index 2987c600d..9c03998fd 100644 --- a/frontend/src/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.ts +++ b/frontend/src/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.ts @@ -56,7 +56,7 @@ export class SchemaFieldRulesFormComponent implements OnInit { if (field.properties.isContentField) { fieldNames.push(field.name); - for (const nestedField of field.nested) { + for (const nestedField of field.nested || []) { if (nestedField.properties.isContentField) { fieldNames.push(`${field.name}.${nestedField.name}`); } @@ -82,17 +82,18 @@ export class SchemaFieldRulesFormComponent implements OnInit { } const value = this.editForm.submit(); - - if (value) { - this.schemasState.configureFieldRules(this.schema, value) - .subscribe({ - next: () => { - this.editForm.submitCompleted({ noReset: true }); - }, - error: error => { - this.editForm.submitFailed(error); - }, - }); + if (!value) { + return; } + + this.schemasState.configureFieldRules(this.schema, value) + .subscribe({ + next: () => { + this.editForm.submitCompleted({ noReset: true }); + }, + error: error => { + this.editForm.submitFailed(error); + }, + }); } } diff --git a/frontend/src/app/features/schemas/pages/schema/scripts/schema-scripts-form.component.ts b/frontend/src/app/features/schemas/pages/schema/scripts/schema-scripts-form.component.ts index 076acf1ac..d7d0129cd 100644 --- a/frontend/src/app/features/schemas/pages/schema/scripts/schema-scripts-form.component.ts +++ b/frontend/src/app/features/schemas/pages/schema/scripts/schema-scripts-form.component.ts @@ -63,17 +63,18 @@ export class SchemaScriptsFormComponent { } const value = this.editForm.submit(); - - if (value) { - this.schemasState.configureScripts(this.schema, value) - .subscribe({ - next: () => { - this.editForm.submitCompleted({ noReset: true }); - }, - error: error => { - this.editForm.submitFailed(error); - }, - }); + if (!value) { + return; } + + this.schemasState.configureScripts(this.schema, value) + .subscribe({ + next: () => { + this.editForm.submitCompleted({ noReset: true }); + }, + error: error => { + this.editForm.submitFailed(error); + }, + }); } } diff --git a/frontend/src/app/features/schemas/pages/schema/ui/field-list.component.ts b/frontend/src/app/features/schemas/pages/schema/ui/field-list.component.ts index 6c6f2420d..1a1638ba6 100644 --- a/frontend/src/app/features/schemas/pages/schema/ui/field-list.component.ts +++ b/frontend/src/app/features/schemas/pages/schema/ui/field-list.component.ts @@ -39,7 +39,7 @@ export class FieldListComponent { public withMetaFields = false; @Output() - public fieldNamesChange = new EventEmitter>(); + public fieldNamesChange = new EventEmitter(); public fieldsAdded!: TableField[]; public fieldsNotAdded!: TableField[]; diff --git a/frontend/src/app/features/schemas/pages/schema/ui/schema-ui-form.component.ts b/frontend/src/app/features/schemas/pages/schema/ui/schema-ui-form.component.ts index 7d55f1742..ba7800529 100644 --- a/frontend/src/app/features/schemas/pages/schema/ui/schema-ui-form.component.ts +++ b/frontend/src/app/features/schemas/pages/schema/ui/schema-ui-form.component.ts @@ -7,7 +7,7 @@ import { Component, Input } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { SchemaDto, SchemasState, TranslatePipe } from '@app/shared'; +import { ConfigureUIFieldsDto, SchemaDto, SchemasState, TranslatePipe } from '@app/shared'; import { FieldListComponent } from './field-list.component'; @Component({ @@ -29,8 +29,8 @@ export class SchemaUIFormComponent { public isEditable = false; - public fieldsInLists: ReadonlyArray = []; - public fieldsInReferences: ReadonlyArray = []; + public fieldsInLists: string[] = []; + public fieldsInReferences: string[] = []; constructor( private readonly schemasState: SchemasState, @@ -44,11 +44,11 @@ export class SchemaUIFormComponent { this.fieldsInReferences = this.schema.fieldsInReferences; } - public setFieldsInLists(names: ReadonlyArray) { + public setFieldsInLists(names: string[]) { this.fieldsInLists = names; } - public setFieldsInReferences(names: ReadonlyArray) { + public setFieldsInReferences(names: string[]) { this.fieldsInReferences = names; } @@ -61,9 +61,11 @@ export class SchemaUIFormComponent { return; } - this.schemasState.configureUIFields(this.schema, { + const request = new ConfigureUIFieldsDto({ fieldsInLists: this.fieldsInLists, fieldsInReferences: this.fieldsInReferences, }); + + this.schemasState.configureUIFields(this.schema, request); } } diff --git a/frontend/src/app/features/schemas/pages/schemas/schema-form.component.ts b/frontend/src/app/features/schemas/pages/schemas/schema-form.component.ts index 99eccd7e7..033b91087 100644 --- a/frontend/src/app/features/schemas/pages/schemas/schema-form.component.ts +++ b/frontend/src/app/features/schemas/pages/schemas/schema-form.component.ts @@ -74,17 +74,18 @@ export class SchemaFormComponent implements OnInit { public createSchema() { const value = this.createForm.submit(); - - if (value) { - this.schemasState.create(value) - .subscribe({ - next: dto => { - this.emitCreate(dto); - }, - error: error => { - this.createForm.submitFailed(error); - }, - }); + if (!value) { + return; } + + this.schemasState.create(value) + .subscribe({ + next: dto => { + this.emitCreate(dto); + }, + error: error => { + this.createForm.submitFailed(error); + }, + }); } } diff --git a/frontend/src/app/features/schemas/pages/schemas/schemas-page.component.ts b/frontend/src/app/features/schemas/pages/schemas/schemas-page.component.ts index 1a5c200bd..1fdc11008 100644 --- a/frontend/src/app/features/schemas/pages/schemas/schemas-page.component.ts +++ b/frontend/src/app/features/schemas/pages/schemas/schemas-page.component.ts @@ -89,13 +89,14 @@ export class SchemasPageComponent implements OnInit { public addCategory() { const value = this.addCategoryForm.submit(); + if (!value) { + return; + } - if (value) { - try { - this.schemasState.addCategory(value.name); - } finally { - this.addCategoryForm.submitCompleted(); - } + try { + this.schemasState.addCategory(value.name); + } finally { + this.addCategoryForm.submitCompleted(); } } diff --git a/frontend/src/app/features/settings/pages/asset-scripts/asset-scripts-page.component.ts b/frontend/src/app/features/settings/pages/asset-scripts/asset-scripts-page.component.ts index 255a86b02..c43e7be13 100644 --- a/frontend/src/app/features/settings/pages/asset-scripts/asset-scripts-page.component.ts +++ b/frontend/src/app/features/settings/pages/asset-scripts/asset-scripts-page.component.ts @@ -83,17 +83,18 @@ export class AssetScriptsPageComponent implements OnInit { } const value = this.editForm.submit(); - - if (value) { - this.assetScriptsState.update(value) - .subscribe({ - next: () => { - this.editForm.submitCompleted({ noReset: true }); - }, - error: error => { - this.editForm.submitFailed(error); - }, - }); + if (!value) { + return; } + + this.assetScriptsState.update(value) + .subscribe({ + next: () => { + this.editForm.submitCompleted({ noReset: true }); + }, + error: error => { + this.editForm.submitFailed(error); + }, + }); } } diff --git a/frontend/src/app/features/settings/pages/clients/client-add-form.component.ts b/frontend/src/app/features/settings/pages/clients/client-add-form.component.ts index 07b6c5e09..0020d9dc8 100644 --- a/frontend/src/app/features/settings/pages/clients/client-add-form.component.ts +++ b/frontend/src/app/features/settings/pages/clients/client-add-form.component.ts @@ -36,18 +36,19 @@ export class ClientAddFormComponent { public addClient() { const value = this.addClientForm.submit(); - - if (value) { - this.clientsState.attach(value) - .subscribe({ - next: () => { - this.addClientForm.submitCompleted(); - }, - error: error => { - this.addClientForm.submitFailed(error); - }, - }); + if (!value) { + return; } + + this.clientsState.attach(value) + .subscribe({ + next: () => { + this.addClientForm.submitCompleted(); + }, + error: error => { + this.addClientForm.submitFailed(error); + }, + }); } public cancel() { diff --git a/frontend/src/app/features/settings/pages/clients/client.component.html b/frontend/src/app/features/settings/pages/clients/client.component.html index ca5a44c63..8229af181 100644 --- a/frontend/src/app/features/settings/pages/clients/client.component.html +++ b/frontend/src/app/features/settings/pages/clients/client.component.html @@ -124,7 +124,7 @@
@if (client.canUpdate) {
-
diff --git a/frontend/src/app/features/settings/pages/clients/client.component.ts b/frontend/src/app/features/settings/pages/clients/client.component.ts index 60a72b962..963269290 100644 --- a/frontend/src/app/features/settings/pages/clients/client.component.ts +++ b/frontend/src/app/features/settings/pages/clients/client.component.ts @@ -8,7 +8,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { AppsState, ClientDto, ClientsState, ConfirmClickDirective, CopyDirective, DialogModel, EditableTitleComponent, FormHintComponent, ModalDirective, RoleDto, TooltipDirective, TourStepDirective, TranslatePipe, TypedSimpleChanges } from '@app/shared'; +import { AppsState, ClientDto, ClientsState, ConfirmClickDirective, CopyDirective, DialogModel, EditableTitleComponent, FormHintComponent, ModalDirective, RoleDto, TooltipDirective, TourStepDirective, TranslatePipe, TypedSimpleChanges, UpdateClientDto } from '@app/shared'; import { ClientConnectFormComponent } from './client-connect-form.component'; @Component({ @@ -58,18 +58,26 @@ export class ClientComponent { } public updateRole(role: string) { - this.clientsState.update(this.client, { role }); + const request = new UpdateClientDto({ role }); + + this.clientsState.update(this.client, request); } public updateAllowAnonymous(allowAnonymous: boolean) { - this.clientsState.update(this.client, { allowAnonymous }); + const request = new UpdateClientDto({ allowAnonymous }); + + this.clientsState.update(this.client, request); } - public updateApiCallsLimit() { - this.clientsState.update(this.client, { apiCallsLimit: this.apiCallsLimit }); + public updateApiCallsLimit(apiCallsLimit: number) { + const request = new UpdateClientDto({ apiCallsLimit }); + + this.clientsState.update(this.client, request); } public rename(name: string) { - this.clientsState.update(this.client, { name }); + const request = new UpdateClientDto({ name }); + + this.clientsState.update(this.client, request); } } diff --git a/frontend/src/app/features/settings/pages/contributors/contributor-add-form.component.ts b/frontend/src/app/features/settings/pages/contributors/contributor-add-form.component.ts index 7c15e9273..6d4972f84 100644 --- a/frontend/src/app/features/settings/pages/contributors/contributor-add-form.component.ts +++ b/frontend/src/app/features/settings/pages/contributors/contributor-add-form.component.ts @@ -85,23 +85,24 @@ export class ContributorAddFormComponent { public assignContributor() { const value = this.assignContributorForm.submit(); + if (!value) { + return; + } - if (value) { - this.contributorsState.assign(value) - .subscribe({ - next: isCreated => { - this.assignContributorForm.submitCompleted({ newValue: this.defaultValue }); + this.contributorsState.assign(value) + .subscribe({ + next: isCreated => { + this.assignContributorForm.submitCompleted({ newValue: this.defaultValue }); - if (isCreated) { - this.dialogs.notifyInfo('i18n:contributors.contributorAssigned'); - } else { - this.dialogs.notifyInfo('i18n:contributors.contributorAssignedOld'); - } - }, - error: error => { - this.assignContributorForm.submitFailed(error); - }, - }); - } + if (isCreated) { + this.dialogs.notifyInfo('i18n:contributors.contributorAssigned'); + } else { + this.dialogs.notifyInfo('i18n:contributors.contributorAssignedOld'); + } + }, + error: error => { + this.assignContributorForm.submitFailed(error); + }, + }); } } diff --git a/frontend/src/app/features/settings/pages/contributors/contributor.component.ts b/frontend/src/app/features/settings/pages/contributors/contributor.component.ts index 6d3cd416b..6647e8276 100644 --- a/frontend/src/app/features/settings/pages/contributors/contributor.component.ts +++ b/frontend/src/app/features/settings/pages/contributors/contributor.component.ts @@ -10,7 +10,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { ConfirmClickDirective, ContributorDto, ContributorsState, HighlightPipe, RoleDto, TooltipDirective, UserPicturePipe } from '@app/shared'; +import { AssignContributorDto, ConfirmClickDirective, ContributorDto, ContributorsState, HighlightPipe, RoleDto, TooltipDirective, UserPicturePipe } from '@app/shared'; @Component({ standalone: true, @@ -46,6 +46,8 @@ export class ContributorComponent { } public changeRole(role: string) { - this.contributorsState.assign({ contributorId: this.contributor.contributorId, role }); + const request = new AssignContributorDto({ contributorId: this.contributor.contributorId, role }); + + this.contributorsState.assign(request); } } diff --git a/frontend/src/app/features/settings/pages/contributors/import-contributors-dialog.component.ts b/frontend/src/app/features/settings/pages/contributors/import-contributors-dialog.component.ts index 22e8d74ce..954413206 100644 --- a/frontend/src/app/features/settings/pages/contributors/import-contributors-dialog.component.ts +++ b/frontend/src/app/features/settings/pages/contributors/import-contributors-dialog.component.ts @@ -10,7 +10,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { EMPTY, of } from 'rxjs'; import { catchError, mergeMap, tap } from 'rxjs/operators'; -import { ContributorsState, ErrorDto, FormHintComponent, ImportContributorsForm, ModalDialogComponent, RoleDto, StatusIconComponent, TooltipDirective, TranslatePipe } from '@app/shared'; +import { AssignContributorDto, ContributorsState, ErrorDto, FormHintComponent, ImportContributorsForm, ModalDialogComponent, RoleDto, StatusIconComponent, TooltipDirective, TranslatePipe } from '@app/shared'; type ImportStatus = { email: string; @@ -96,7 +96,7 @@ export class ImportContributorsDialogComponent { } function createRequest(status: ImportStatus) { - return { contributorId: status.email, role: status.role, invite: true }; + return new AssignContributorDto({ contributorId: status.email, role: status.role, invite: true }); } function getError(error: ErrorDto): string { diff --git a/frontend/src/app/features/settings/pages/languages/language-add-form.component.ts b/frontend/src/app/features/settings/pages/languages/language-add-form.component.ts index 223c2820c..3d121b014 100644 --- a/frontend/src/app/features/settings/pages/languages/language-add-form.component.ts +++ b/frontend/src/app/features/settings/pages/languages/language-add-form.component.ts @@ -69,17 +69,18 @@ export class LanguageAddFormComponent { public addLanguage() { const value = this.addLanguageForm.submit(); - - if (value) { - this.languagesState.add(value.language) - .subscribe({ - next: () => { - this.addLanguageForm.submitCompleted(); - }, - error: error => { - this.addLanguageForm.submitFailed(error); - }, - }); + if (!value) { + return; } + + this.languagesState.add(value.language) + .subscribe({ + next: () => { + this.addLanguageForm.submitCompleted(); + }, + error: error => { + this.addLanguageForm.submitFailed(error); + }, + }); } } diff --git a/frontend/src/app/features/settings/pages/languages/language.component.ts b/frontend/src/app/features/settings/pages/languages/language.component.ts index fb6385139..d3a950622 100644 --- a/frontend/src/app/features/settings/pages/languages/language.component.ts +++ b/frontend/src/app/features/settings/pages/languages/language.component.ts @@ -9,7 +9,7 @@ import { CdkDrag, CdkDragDrop, CdkDragHandle, CdkDropList } from '@angular/cdk/d import { Component, Input } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { AppLanguageDto, ConfirmClickDirective, EditLanguageForm, FormHintComponent, LanguageDto, LanguagesState, sorted, TranslatePipe } from '@app/shared'; +import { AppLanguageDto, ConfirmClickDirective, EditLanguageForm, FormHintComponent, LanguageDto, LanguagesState, sorted, TranslatePipe, UpdateLanguageDto } from '@app/shared'; @Component({ standalone: true, @@ -67,7 +67,7 @@ export class LanguageComponent { } public sort(event: CdkDragDrop>) { - this.fallbackLanguages = sorted(event); + this.fallbackLanguages = sorted(event) as any; } public save() { @@ -76,20 +76,21 @@ export class LanguageComponent { } const value = this.editForm.submit(); - - if (value) { - const request = { ...value, fallback: this.fallbackLanguages.map(x => x.iso2Code) }; - - this.languagesState.update(this.language, request) - .subscribe({ - next: () => { - this.editForm.submitCompleted({ noReset: true }); - }, - error: error => { - this.editForm.submitFailed(error); - }, - }); + if (!value) { + return; } + + const request = new UpdateLanguageDto({ ...value, fallback: this.fallbackLanguages.map(x => x.iso2Code) }); + + this.languagesState.update(this.language, request) + .subscribe({ + next: () => { + this.editForm.submitCompleted({ noReset: true }); + }, + error: error => { + this.editForm.submitFailed(error); + }, + }); } public removeFallbackLanguage(language: LanguageDto) { diff --git a/frontend/src/app/features/settings/pages/more/more-page.component.ts b/frontend/src/app/features/settings/pages/more/more-page.component.ts index 55bf1d98b..6b4e3aeb4 100644 --- a/frontend/src/app/features/settings/pages/more/more-page.component.ts +++ b/frontend/src/app/features/settings/pages/more/more-page.component.ts @@ -92,20 +92,21 @@ export class MorePageComponent implements OnInit { } const value = this.updateForm.submit(); - - if (value) { - this.appsState.update(this.app, value) - .subscribe({ - next: app => { - this.updateForm.submitCompleted({ newValue: app }); - }, - error: error => { - this.dialogs.notifyError(error); - - this.updateForm.submitFailed(error); - }, - }); + if (!value) { + return; } + + this.appsState.update(this.app, value) + .subscribe({ + next: app => { + this.updateForm.submitCompleted({ newValue: app }); + }, + error: error => { + this.dialogs.notifyError(error); + + this.updateForm.submitFailed(error); + }, + }); } public transfer() { @@ -114,20 +115,21 @@ export class MorePageComponent implements OnInit { } const value = this.transferForm.submit(); - - if (value) { - this.appsState.transfer(this.app, value.teamId) - .subscribe({ - next: app => { - this.transferForm.submitCompleted({ newValue: app }); - }, - error: error => { - this.dialogs.notifyError(error); - - this.transferForm.submitFailed(error); - }, - }); + if (!value) { + return; } + + this.appsState.transfer(this.app, value) + .subscribe({ + next: app => { + this.transferForm.submitCompleted({ newValue: app }); + }, + error: error => { + this.dialogs.notifyError(error); + + this.transferForm.submitFailed(error); + }, + }); } public uploadImage(file: ReadonlyArray) { diff --git a/frontend/src/app/features/settings/pages/plans/plan.component.ts b/frontend/src/app/features/settings/pages/plans/plan.component.ts index b111255d2..2ddc7a672 100644 --- a/frontend/src/app/features/settings/pages/plans/plan.component.ts +++ b/frontend/src/app/features/settings/pages/plans/plan.component.ts @@ -38,6 +38,6 @@ export class PlanComponent { } public changeYearly() { - this.plansState.change(this.planInfo.plan.yearlyId); + this.plansState.change(this.planInfo.plan.yearlyId!); } } diff --git a/frontend/src/app/features/settings/pages/roles/role-add-form.component.ts b/frontend/src/app/features/settings/pages/roles/role-add-form.component.ts index 0a4c4331c..4a3ca6416 100644 --- a/frontend/src/app/features/settings/pages/roles/role-add-form.component.ts +++ b/frontend/src/app/features/settings/pages/roles/role-add-form.component.ts @@ -35,18 +35,19 @@ export class RoleAddFormComponent { public addRole() { const value = this.addRoleForm.submit(); - - if (value) { - this.rolesState.add(value) - .subscribe({ - next: () => { - this.addRoleForm.submitCompleted(); - }, - error: error => { - this.addRoleForm.submitFailed(error); - }, - }); + if (!value) { + return; } + + this.rolesState.add(value) + .subscribe({ + next: () => { + this.addRoleForm.submitCompleted(); + }, + error: error => { + this.addRoleForm.submitFailed(error); + }, + }); } public cancel() { diff --git a/frontend/src/app/features/settings/pages/roles/role.component.ts b/frontend/src/app/features/settings/pages/roles/role.component.ts index cf2a8dc5e..347dd9bc6 100644 --- a/frontend/src/app/features/settings/pages/roles/role.component.ts +++ b/frontend/src/app/features/settings/pages/roles/role.component.ts @@ -8,7 +8,7 @@ import { SlicePipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, Input, ViewChild } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { AutocompleteComponent, AutocompleteSource, ConfirmClickDirective, ControlErrorsComponent, EditRoleForm, FormAlertComponent, FormHintComponent, RoleDto, RolesState, SchemaDto, Settings, TranslatePipe, TypedSimpleChanges } from '@app/shared'; +import { AutocompleteComponent, AutocompleteSource, ConfirmClickDirective, ControlErrorsComponent, EditRoleForm, FormAlertComponent, FormHintComponent, RoleDto, RolesState, SchemaDto, Settings, TranslatePipe, TypedSimpleChanges, UpdateRoleDto } from '@app/shared'; const DESCRIPTIONS = { Developer: 'i18n:roles.defaults.developer', @@ -117,17 +117,20 @@ export class RoleComponent { public save() { const value = this.editForm.submit(); - - if (value) { - this.rolesState.update(this.role, { ...value, properties: this.properties }) - .subscribe({ - next: () => { - this.editForm.submitCompleted({ noReset: true }); - }, - error: error => { - this.editForm.submitFailed(error); - }, - }); + if (!value) { + return; } + + const request = new UpdateRoleDto({ ...value, properties: this.properties }); + + this.rolesState.update(this.role, request) + .subscribe({ + next: () => { + this.editForm.submitCompleted({ noReset: true }); + }, + error: error => { + this.editForm.submitFailed(error); + }, + }); } } diff --git a/frontend/src/app/features/settings/pages/settings/settings-page.component.html b/frontend/src/app/features/settings/pages/settings/settings-page.component.html index 72f64985d..03577ef10 100644 --- a/frontend/src/app/features/settings/pages/settings/settings-page.component.html +++ b/frontend/src/app/features/settings/pages/settings/settings-page.component.html @@ -28,7 +28,7 @@
@if (!isEditable && editForm.patternsControls.length === 0) { -
+
{{ "appSettings.patterns.empty" | sqxTranslate }}
} @@ -109,7 +109,7 @@
@if (!isEditable && editForm.editorsControls.length === 0) { -
+
{{ "appSettings.editors.empty" | sqxTranslate }}
} diff --git a/frontend/src/app/features/settings/pages/settings/settings-page.component.ts b/frontend/src/app/features/settings/pages/settings/settings-page.component.ts index dbc895e7b..8281587a5 100644 --- a/frontend/src/app/features/settings/pages/settings/settings-page.component.ts +++ b/frontend/src/app/features/settings/pages/settings/settings-page.component.ts @@ -75,17 +75,18 @@ export class SettingsPageComponent implements OnInit { } const value = this.editForm.submit(); - - if (value) { - this.appsState.updateSettings(this.editingSettings, value) - .subscribe({ - next: () => { - this.editForm.submitCompleted({ noReset: true }); - }, - error: error => { - this.editForm.submitFailed(error); - }, - }); + if (!value) { + return; } + + this.appsState.updateSettings(this.editingSettings, value) + .subscribe({ + next: () => { + this.editForm.submitCompleted({ noReset: true }); + }, + error: error => { + this.editForm.submitFailed(error); + }, + }); } } diff --git a/frontend/src/app/features/settings/pages/workflows/workflow-add-form.component.ts b/frontend/src/app/features/settings/pages/workflows/workflow-add-form.component.ts index 5782a601e..df21e70f1 100644 --- a/frontend/src/app/features/settings/pages/workflows/workflow-add-form.component.ts +++ b/frontend/src/app/features/settings/pages/workflows/workflow-add-form.component.ts @@ -35,18 +35,19 @@ export class WorkflowAddFormComponent { public addWorkflow() { const value = this.addWorkflowForm.submit(); - - if (value) { - this.workflowsState.add(value.name) - .subscribe({ - next: () => { - this.addWorkflowForm.submitCompleted(); - }, - error: error => { - this.addWorkflowForm.submitFailed(error); - }, - }); + if (!value) { + return; } + + this.workflowsState.add(value) + .subscribe({ + next: () => { + this.addWorkflowForm.submitCompleted(); + }, + error: error => { + this.addWorkflowForm.submitFailed(error); + }, + }); } public cancel() { diff --git a/frontend/src/app/features/settings/pages/workflows/workflow-diagram.component.ts b/frontend/src/app/features/settings/pages/workflows/workflow-diagram.component.ts index 2d39adbc8..9e5eefd46 100644 --- a/frontend/src/app/features/settings/pages/workflows/workflow-diagram.component.ts +++ b/frontend/src/app/features/settings/pages/workflows/workflow-diagram.component.ts @@ -6,7 +6,7 @@ */ import { AfterViewInit, Component, ElementRef, Input, OnDestroy, ViewChild } from '@angular/core'; -import { ListViewComponent, ResourceLoaderService, WorkflowDto } from '@app/shared'; +import { ListViewComponent, ResourceLoaderService, WorkflowView } from '@app/shared'; declare const vis: any; @@ -26,7 +26,7 @@ export class WorkflowDiagramComponent implements AfterViewInit, OnDestroy { public chartContainer!: ElementRef; @Input({ required: true }) - public workflow!: WorkflowDto; + public workflow!: WorkflowView; public isLoaded = false; @@ -65,21 +65,21 @@ export class WorkflowDiagramComponent implements AfterViewInit, OnDestroy { } } -function buildGraph(workflow: WorkflowDto) { +function buildGraph(workflow: WorkflowView) { const nodes = new vis.DataSet(); for (const step of workflow.steps) { let label = `${step.name}`; - if (step.noUpdate) { + if (step.values.noUpdate) { label += '\nPrevent updates'; - if (step.noUpdateExpression) { - label += `\nwhen (${step.noUpdateExpression})`; + if (step.values.noUpdateExpression) { + label += `\nwhen (${step.values.noUpdateExpression})`; } - if (step.noUpdateRoles && step.noUpdateRoles.length > 0) { - label += `\nfor ${step.noUpdateRoles.join(', ')}`; + if (step.values.noUpdateRoles && step.values.noUpdateRoles.length > 0) { + label += `\nfor ${step.values.noUpdateRoles.join(', ')}`; } } @@ -87,12 +87,12 @@ function buildGraph(workflow: WorkflowDto) { label += '\nAvailable in the API'; } - const node: any = { id: step.name, label, color: step.color }; + const node: any = { id: step.name, label, color: step.values.color }; nodes.add(node); } - if (workflow.initial) { + if (workflow.dto.initial) { nodes.add({ id: 0, color: '#000', label: 'Start', shape: 'dot', size: 3 }); } @@ -101,12 +101,12 @@ function buildGraph(workflow: WorkflowDto) { for (const transition of workflow.transitions) { let label = ''; - if (transition.expression) { - label += `\nwhen (${transition.expression})`; + if (transition.values.expression) { + label += `\nwhen (${transition.values.expression})`; } - if (transition.roles && transition.roles.length > 0) { - label += `\nfor ${transition.roles.join(', ')}`; + if (transition.values.roles && transition.values.roles.length > 0) { + label += `\nfor ${transition.values.roles.join(', ')}`; } const edge: any = { ...transition, label }; @@ -114,8 +114,8 @@ function buildGraph(workflow: WorkflowDto) { edges.add(edge); } - if (workflow.initial) { - edges.add({ from: 0, to: workflow.initial }); + if (workflow.dto.initial) { + edges.add({ from: 0, to: workflow.dto.initial }); } return { edges, nodes }; diff --git a/frontend/src/app/features/settings/pages/workflows/workflow-step.component.html b/frontend/src/app/features/settings/pages/workflows/workflow-step.component.html index 38dbd9e6f..126a8d246 100644 --- a/frontend/src/app/features/settings/pages/workflows/workflow-step.component.html +++ b/frontend/src/app/features/settings/pages/workflows/workflow-step.component.html @@ -3,10 +3,10 @@
@@ -60,7 +60,7 @@
- + {{ target.name }} @@ -81,7 +81,7 @@ class="form-check-input step-prevent-updates-checkbox" id="preventUpdates_{{ step.name }}" [disabled]="!!disabled" - [ngModel]="step.noUpdate" + [ngModel]="step.values.noUpdate" (ngModelChange)="changeNoUpdate($event)" type="checkbox" /> @@ -90,16 +90,16 @@
- @if (step.noUpdate) { + @if (step.values.noUpdate) {
{{ "workflows.syntax.when" | sqxTranslate }}
diff --git a/frontend/src/app/features/settings/pages/workflows/workflow-step.component.ts b/frontend/src/app/features/settings/pages/workflows/workflow-step.component.ts index d7b5f283a..9cd37dda9 100644 --- a/frontend/src/app/features/settings/pages/workflows/workflow-step.component.ts +++ b/frontend/src/app/features/settings/pages/workflows/workflow-step.component.ts @@ -8,7 +8,7 @@ import { booleanAttribute, Component, EventEmitter, Input, Output } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { ColorPickerComponent, DropdownComponent, EditableTitleComponent, TagEditorComponent, TranslatePipe, TypedSimpleChanges, WorkflowDto, WorkflowStep, WorkflowStepValues, WorkflowTransition, WorkflowTransitionValues, WorkflowTransitionView } from '@app/shared'; +import { ColorPickerComponent, DropdownComponent, EditableTitleComponent, TagEditorComponent, TranslatePipe, TypedSimpleChanges, WorkflowStepValues, WorkflowStepView, WorkflowTransitionValues, WorkflowTransitionView, WorkflowView } from '@app/shared'; import { WorkflowTransitionComponent } from './workflow-transition.component'; @Component({ @@ -33,13 +33,13 @@ export class WorkflowStepComponent { public makeInitial = new EventEmitter(); @Output() - public transitionAdd = new EventEmitter(); + public transitionAdd = new EventEmitter(); @Output() - public transitionRemove = new EventEmitter(); + public transitionRemove = new EventEmitter(); @Output() - public transitionUpdate = new EventEmitter<{ transition: WorkflowTransition; values: WorkflowTransitionValues }>(); + public transitionUpdate = new EventEmitter<{ transition: WorkflowTransitionView; values: WorkflowTransitionValues }>(); @Output() public update = new EventEmitter(); @@ -51,10 +51,10 @@ export class WorkflowStepComponent { public remove = new EventEmitter(); @Input({ required: true }) - public workflow!: WorkflowDto; + public workflow!: WorkflowView; @Input({ required: true }) - public step!: WorkflowStep; + public step!: WorkflowStepView; @Input({ required: true }) public roles!: ReadonlyArray; @@ -62,8 +62,8 @@ export class WorkflowStepComponent { @Input({ transform: booleanAttribute }) public disabled?: boolean | null; - public openSteps!: ReadonlyArray; - public openStep!: WorkflowStep; + public openSteps!: ReadonlyArray; + public openStep!: WorkflowStepView; public transitions!: ReadonlyArray; @@ -76,7 +76,7 @@ export class WorkflowStepComponent { } } - public changeTransition(transition: WorkflowTransition, values: WorkflowTransitionValues) { + public changeTransition(transition: WorkflowTransitionView, values: WorkflowTransitionValues) { this.transitionUpdate.emit({ transition, values }); } @@ -100,7 +100,7 @@ export class WorkflowStepComponent { this.update.emit({ noUpdateExpression }); } - public changeNoUpdateRoles(noUpdateRoles?: ReadonlyArray) { + public changeNoUpdateRoles(noUpdateRoles?: string[]) { this.update.emit({ noUpdateRoles }); } } diff --git a/frontend/src/app/features/settings/pages/workflows/workflow-transition.component.html b/frontend/src/app/features/settings/pages/workflows/workflow-transition.component.html index a65c50647..13a54971e 100644 --- a/frontend/src/app/features/settings/pages/workflows/workflow-transition.component.html +++ b/frontend/src/app/features/settings/pages/workflows/workflow-transition.component.html @@ -4,7 +4,7 @@
- + {{ transition.to }}
@@ -14,9 +14,9 @@
- {{ workflow.displayName }} + {{ workflowView.dto.displayName }}
@@ -27,7 +27,7 @@ confirmRememberKey="deleteWorkflow" confirmText="i18n:workflows.deleteConfirmText" confirmTitle="i18n:workflows.deleteConfirmTitle" - [disabled]="!workflow.canDelete" + [disabled]="!workflowView.dto.canDelete" (sqxConfirmClick)="remove()" type="button"> @@ -66,13 +66,13 @@ @if (selectedTab === 0) {
- +
@@ -81,13 +81,13 @@
- +
@@ -95,9 +95,9 @@
- @for (step of workflow.steps; track step.name) { + @for (step of workflowView.steps; track step.name) { + [workflow]="workflowView"> } - @if (workflow.canUpdate) { + @if (workflowView.dto.canUpdate) { } } @else { - + }
diff --git a/frontend/src/app/features/settings/pages/workflows/workflow.component.ts b/frontend/src/app/features/settings/pages/workflows/workflow.component.ts index c935e1172..cbd2492f0 100644 --- a/frontend/src/app/features/settings/pages/workflows/workflow.component.ts +++ b/frontend/src/app/features/settings/pages/workflows/workflow.component.ts @@ -8,7 +8,7 @@ import { AsyncPipe } from '@angular/common'; import { Component, Input } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { ConfirmClickDirective, ErrorDto, FormErrorComponent, FormHintComponent, MathHelper, SchemaTagSource, TagEditorComponent, TranslatePipe, WorkflowDto, WorkflowsState, WorkflowStep, WorkflowStepValues, WorkflowTransition, WorkflowTransitionValues } from '@app/shared'; +import { ConfirmClickDirective, ErrorDto, FormErrorComponent, FormHintComponent, IWorkflowStepDto, IWorkflowTransitionDto, MathHelper, SchemaTagSource, TagEditorComponent, TranslatePipe, WorkflowDto, WorkflowsState, WorkflowStepView, WorkflowTransitionView, WorkflowView } from '@app/shared'; import { WorkflowDiagramComponent } from './workflow-diagram.component'; import { WorkflowStepComponent } from './workflow-step.component'; @@ -33,7 +33,9 @@ export class WorkflowComponent { public readonly onBlur: { updateOn: 'blur' } = { updateOn: 'blur' }; @Input({ required: true }) - public workflow!: WorkflowDto; + public set workflow(value: WorkflowDto) { + this.workflowView = new WorkflowView(value); + }; @Input({ required: true }) public roles!: ReadonlyArray; @@ -46,6 +48,8 @@ export class WorkflowComponent { public isEditing = false; public isEditable = false; + public workflowView!: WorkflowView; + public selectedTab = 0; constructor( @@ -54,7 +58,7 @@ export class WorkflowComponent { } public ngOnChanges() { - this.isEditable = this.workflow.canUpdate; + this.isEditable = this.workflowView.dto.canUpdate; } public toggleEditing() { @@ -62,7 +66,7 @@ export class WorkflowComponent { } public remove() { - this.workflowsState.delete(this.workflow); + this.workflowsState.delete(this.workflowView.dto); } public save() { @@ -70,7 +74,7 @@ export class WorkflowComponent { return; } - this.workflowsState.update(this.workflow) + this.workflowsState.update(this.workflowView.dto, this.workflowView.toUpdate()) .subscribe({ next: () => { this.error = null; @@ -82,52 +86,52 @@ export class WorkflowComponent { } public addStep() { - const index = this.workflow.steps.length; + const index = this.workflowView.steps.length; for (let i = index; i < index + 100; i++) { const name = `Step${i}`; - if (!this.workflow.getStep(name)) { - this.workflow = this.workflow.setStep(name, { color: MathHelper.randomColor() }); + if (!this.workflowView.getStep(name)) { + this.workflowView = this.workflowView.setStep(name, { color: MathHelper.randomColor() }); return; } } } public rename(name: string) { - this.workflow = this.workflow.changeName(name); + this.workflowView = this.workflowView.changeName(name); } public changeSchemaIds(schemaIds: string[]) { - this.workflow = this.workflow.changeSchemaIds(schemaIds); + this.workflowView = this.workflowView.changeSchemaIds(schemaIds); } - public setInitial(step: WorkflowStep) { - this.workflow = this.workflow.setInitial(step.name); + public setInitial(step: WorkflowStepView) { + this.workflowView = this.workflowView.setInitial(step.name); } - public addTransiton(from: WorkflowStep, to: WorkflowStep) { - this.workflow = this.workflow.setTransition(from.name, to.name, {}); + public addTransiton(from: WorkflowStepView, to: WorkflowStepView) { + this.workflowView = this.workflowView.setTransition(from.name, to.name, {}); } - public removeTransition(from: WorkflowStep, transition: WorkflowTransition) { - this.workflow = this.workflow.removeTransition(from.name, transition.to); + public removeTransition(from: WorkflowStepView, transition: WorkflowTransitionView) { + this.workflowView = this.workflowView.removeTransition(from.name, transition.to); } - public updateTransition(update: { transition: WorkflowTransition; values: WorkflowTransitionValues }) { - this.workflow = this.workflow.setTransition(update.transition.from, update.transition.to, update.values); + public updateTransition(update: { transition: WorkflowTransitionView; values: IWorkflowTransitionDto }) { + this.workflowView = this.workflowView.setTransition(update.transition.from, update.transition.to, update.values); } - public updateStep(step: WorkflowStep, values: WorkflowStepValues) { - this.workflow = this.workflow.setStep(step.name, values); + public updateStep(step: WorkflowStepView, values: Partial) { + this.workflowView = this.workflowView.setStep(step.name, values); } - public renameStep(step: WorkflowStep, newName: string) { - this.workflow = this.workflow.renameStep(step.name, newName); + public renameStep(step: WorkflowStepView, newName: string) { + this.workflowView = this.workflowView.renameStep(step.name, newName); } - public removeStep(step: WorkflowStep) { - this.workflow = this.workflow.removeStep(step.name); + public removeStep(step: WorkflowStepView) { + this.workflowView = this.workflowView.removeStep(step.name); } public selectTab(tab: number) { diff --git a/frontend/src/app/features/teams/pages/auth/auth-page.component.ts b/frontend/src/app/features/teams/pages/auth/auth-page.component.ts index 48ac5b5e9..6a34b5a67 100644 --- a/frontend/src/app/features/teams/pages/auth/auth-page.component.ts +++ b/frontend/src/app/features/teams/pages/auth/auth-page.component.ts @@ -84,9 +84,8 @@ export class AuthPageComponent implements OnInit { } this.isEditing = isEditing; - if (!isEditing) { - this.authState.update(null); + this.authState.update(undefined); } } @@ -96,17 +95,18 @@ export class AuthPageComponent implements OnInit { } const value = this.updateForm.submit(); - - if (value) { - this.authState.update(value) - .subscribe({ - next: scheme => { - this.updateForm.submitCompleted({ newValue: scheme || {} as any }); - }, - error: error => { - this.updateForm.submitFailed(error); - }, - }); + if (!value) { + return; } + + this.authState.update(value) + .subscribe({ + next: scheme => { + this.updateForm.submitCompleted({ newValue: scheme || {} }); + }, + error: error => { + this.updateForm.submitFailed(error); + }, + }); } } diff --git a/frontend/src/app/features/teams/pages/contributors/contributor-add-form.component.ts b/frontend/src/app/features/teams/pages/contributors/contributor-add-form.component.ts index 768db9f1c..ebe7c2288 100644 --- a/frontend/src/app/features/teams/pages/contributors/contributor-add-form.component.ts +++ b/frontend/src/app/features/teams/pages/contributors/contributor-add-form.component.ts @@ -78,23 +78,24 @@ export class ContributorAddFormComponent { public assignContributor() { const value = this.assignContributorForm.submit(); + if (!value) { + return; + } - if (value) { - this.contributorsState.assign(value) - .subscribe({ - next: isCreated => { - this.assignContributorForm.submitCompleted({ newValue: { user: '' } as any }); + this.contributorsState.assign(value) + .subscribe({ + next: isCreated => { + this.assignContributorForm.submitCompleted({ newValue: { user: '' } }); - if (isCreated) { - this.dialogs.notifyInfo('i18n:contributors.contributorAssigned'); - } else { - this.dialogs.notifyInfo('i18n:contributors.contributorAssignedOld'); - } - }, - error: error => { - this.assignContributorForm.submitFailed(error); - }, - }); - } + if (isCreated) { + this.dialogs.notifyInfo('i18n:contributors.contributorAssigned'); + } else { + this.dialogs.notifyInfo('i18n:contributors.contributorAssignedOld'); + } + }, + error: error => { + this.assignContributorForm.submitFailed(error); + }, + }); } } diff --git a/frontend/src/app/features/teams/pages/contributors/contributor.component.ts b/frontend/src/app/features/teams/pages/contributors/contributor.component.ts index 8bc3d0640..e9574bec6 100644 --- a/frontend/src/app/features/teams/pages/contributors/contributor.component.ts +++ b/frontend/src/app/features/teams/pages/contributors/contributor.component.ts @@ -8,7 +8,7 @@ /* eslint-disable @angular-eslint/component-selector */ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; -import { ConfirmClickDirective, ContributorDto, HighlightPipe, TooltipDirective, UserPicturePipe } from '@app/shared'; +import { AssignContributorDto, ConfirmClickDirective, ContributorDto, HighlightPipe, TooltipDirective, UserPicturePipe } from '@app/shared'; import { TeamContributorsState } from '../../internal'; @Component({ @@ -41,6 +41,8 @@ export class ContributorComponent { } public changeRole(role: string) { - this.contributorsState.assign({ contributorId: this.contributor.contributorId, role }); + const request = new AssignContributorDto({ contributorId: this.contributor.contributorId, role }); + + this.contributorsState.assign(request); } } diff --git a/frontend/src/app/features/teams/pages/contributors/import-contributors-dialog.component.ts b/frontend/src/app/features/teams/pages/contributors/import-contributors-dialog.component.ts index fd204b7e4..aa5a6ca4a 100644 --- a/frontend/src/app/features/teams/pages/contributors/import-contributors-dialog.component.ts +++ b/frontend/src/app/features/teams/pages/contributors/import-contributors-dialog.component.ts @@ -10,7 +10,7 @@ import { Component, EventEmitter, Output } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { EMPTY, of } from 'rxjs'; import { catchError, mergeMap, tap } from 'rxjs/operators'; -import { ErrorDto, FormHintComponent, ImportContributorsForm, ModalDialogComponent, StatusIconComponent, TooltipDirective, TranslatePipe } from '@app/shared'; +import { AssignContributorDto, ErrorDto, FormHintComponent, ImportContributorsForm, ModalDialogComponent, StatusIconComponent, TooltipDirective, TranslatePipe } from '@app/shared'; import { TeamContributorsState } from '../../internal'; type ImportStatus = { @@ -94,7 +94,7 @@ export class ImportContributorsDialogComponent { } function createRequest(status: ImportStatus) { - return { contributorId: status.email, role: status.role, invite: true }; + return new AssignContributorDto({ contributorId: status.email, role: status.role, invite: true }); } function getError(error: ErrorDto): string { diff --git a/frontend/src/app/features/teams/pages/more/more-page.component.ts b/frontend/src/app/features/teams/pages/more/more-page.component.ts index e01c7793e..dfc6c1ece 100644 --- a/frontend/src/app/features/teams/pages/more/more-page.component.ts +++ b/frontend/src/app/features/teams/pages/more/more-page.component.ts @@ -73,18 +73,19 @@ export class MorePageComponent implements OnInit { } const value = this.updateForm.submit(); - - if (value) { - this.teamsState.update(this.team, value) - .subscribe({ - next: team => { - this.updateForm.submitCompleted({ newValue: team }); - }, - error: error => { - this.updateForm.submitFailed(error); - }, - }); + if (!value) { + return; } + + this.teamsState.update(this.team, value) + .subscribe({ + next: team => { + this.updateForm.submitCompleted({ newValue: team }); + }, + error: error => { + this.updateForm.submitFailed(error); + }, + }); } public deleteTeam() { diff --git a/frontend/src/app/features/teams/pages/plans/plan.component.ts b/frontend/src/app/features/teams/pages/plans/plan.component.ts index d16cf1b01..ff173f050 100644 --- a/frontend/src/app/features/teams/pages/plans/plan.component.ts +++ b/frontend/src/app/features/teams/pages/plans/plan.component.ts @@ -39,6 +39,6 @@ export class PlanComponent { } public changeYearly() { - this.plansState.change(this.planInfo.plan.yearlyId); + this.plansState.change(this.planInfo.plan.yearlyId!); } } diff --git a/frontend/src/app/features/teams/services/team-contributors.service.spec.ts b/frontend/src/app/features/teams/services/team-contributors.service.spec.ts index 3db45776c..f1853578e 100644 --- a/frontend/src/app/features/teams/services/team-contributors.service.spec.ts +++ b/frontend/src/app/features/teams/services/team-contributors.service.spec.ts @@ -8,11 +8,12 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { ApiUrlConfig, ContributorDto, ContributorsDto, ContributorsPayload, Resource, ResourceLinks, Version } from '@app/shared/internal'; +import { ApiUrlConfig, ContributorDto, ContributorsDto, Resource, Versioned, VersionTag } from '@app/shared/internal'; +import { AssignContributorDto, ContributorsMetadataDto, ResourceLinkDto } from '@app/shared/model'; import { TeamContributorsService } from '../internal'; describe('TeamContributorsService', () => { - const version = new Version('1'); + const version = new VersionTag('1'); beforeEach(() => { TestBed.configureTestingModule({ @@ -32,8 +33,7 @@ describe('TeamContributorsService', () => { it('should make get request to get team contributors', inject([TeamContributorsService, HttpTestingController], (contributorsService: TeamContributorsService, httpMock: HttpTestingController) => { - let contributors: ContributorsDto; - + let contributors: Versioned; contributorsService.getContributors('my-team').subscribe(result => { contributors = result; }); @@ -49,15 +49,14 @@ describe('TeamContributorsService', () => { }, }); - expect(contributors!).toEqual({ payload: createContributors(1, 2, 3), version: new Version('2') }); + expect(contributors!).toEqual({ payload: createContributors(1, 2, 3), version: new VersionTag('2') }); })); it('should make post request to assign contributor', inject([TeamContributorsService, HttpTestingController], (contributorsService: TeamContributorsService, httpMock: HttpTestingController) => { - const dto = { contributorId: '123', role: 'Owner' }; - - let contributors: ContributorsDto; + const dto = new AssignContributorDto({ contributorId: '123', role: 'Owner' }); + let contributors: Versioned; contributorsService.postContributor('my-team', dto, version).subscribe(result => { contributors = result; }); @@ -73,7 +72,7 @@ describe('TeamContributorsService', () => { }, }); - expect(contributors!).toEqual({ payload: createContributors(1, 2, 3), version: new Version('2') }); + expect(contributors!).toEqual({ payload: createContributors(1, 2, 3), version: new VersionTag('2') }); })); it('should make delete request to remove contributor', @@ -84,8 +83,7 @@ describe('TeamContributorsService', () => { }, }; - let contributors: ContributorsDto; - + let contributors: Versioned; contributorsService.deleteContributor('my-team', resource, version).subscribe(result => { contributors = result; }); @@ -101,7 +99,7 @@ describe('TeamContributorsService', () => { }, }); - expect(contributors!).toEqual({ payload: createContributors(1, 2, 3), version: new Version('2') }); + expect(contributors!).toEqual({ payload: createContributors(1, 2, 3), version: new VersionTag('2') }); })); function contributorsResponse(...ids: number[]) { @@ -126,19 +124,27 @@ describe('TeamContributorsService', () => { } }); -export function createContributors(...ids: ReadonlyArray): ContributorsPayload { - return { - maxContributors: ids.length * 13, +export function createContributors(...ids: ReadonlyArray) { + return new ContributorsDto({ items: ids.map(createContributor), - isInvited: false, - canCreate: true, - }; + maxContributors: ids.length * 13, + _links: { + create: new ResourceLinkDto({ method: 'POST', href: '/contributors' }), + }, + _meta: new ContributorsMetadataDto({ + isInvited: 'true', + }), + }); } export function createContributor(id: number) { - const links: ResourceLinks = { - update: { method: 'PUT', href: `/contributors/id${id}` }, - }; - - return new ContributorDto(links, `id${id}`, `name${id}`, `mail${id}@squidex.io`, id % 2 === 0 ? 'Owner' : 'Developer'); -} + return new ContributorDto({ + contributorId: `id${id}`, + contributorName: `name${id}`, + contributorEmail: `mail${id}@squidex.io`, + role: id % 2 === 0 ? 'Owner' : 'Developer', + _links: { + update: new ResourceLinkDto({ method: 'PUT', href: `/contributors/id${id}` }), + }, + }); +} \ No newline at end of file diff --git a/frontend/src/app/features/teams/services/team-contributors.service.ts b/frontend/src/app/features/teams/services/team-contributors.service.ts index f3d2089fe..6f492e01b 100644 --- a/frontend/src/app/features/teams/services/team-contributors.service.ts +++ b/frontend/src/app/features/teams/services/team-contributors.service.ts @@ -8,7 +8,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { ApiUrlConfig, AssignContributorDto, ContributorsDto, HTTP, mapVersioned, parseContributors, pretifyError, Resource, Version } from '@app/shared'; +import { ApiUrlConfig, AssignContributorDto, ContributorsDto, HTTP, mapVersioned, pretifyError, Resource, Versioned, VersionOrTag } from '@app/shared'; @Injectable({ providedIn: 'any', @@ -20,34 +20,34 @@ export class TeamContributorsService { ) { } - public getContributors(teamId: string): Observable { + public getContributors(teamId: string): Observable> { const url = this.apiUrl.buildUrl(`api/teams/${teamId}/contributors`); return HTTP.getVersioned(this.http, url).pipe( mapVersioned(({ body }) => { - return parseContributors(body); + return ContributorsDto.fromJSON(body); }), pretifyError('i18n:contributors.loadFailed')); } - public postContributor(teamId: string, dto: AssignContributorDto, version: Version): Observable { + public postContributor(teamId: string, dto: AssignContributorDto, version: VersionOrTag): Observable> { const url = this.apiUrl.buildUrl(`api/teams/${teamId}/contributors`); - return HTTP.postVersioned(this.http, url, dto, version).pipe( + return HTTP.postVersioned(this.http, url, dto.toJSON(), version).pipe( mapVersioned(({ body }) => { - return parseContributors(body); + return ContributorsDto.fromJSON(body); }), pretifyError('i18n:contributors.addFailed')); } - public deleteContributor(teamId: string, resource: Resource, version: Version): Observable { + public deleteContributor(teamId: string, resource: Resource, version: VersionOrTag): Observable> { const link = resource._links['delete']; const url = this.apiUrl.buildUrl(link.href); return HTTP.requestVersioned(this.http, link.method, url, version).pipe( mapVersioned(({ body }) => { - return parseContributors(body); + return ContributorsDto.fromJSON(body); }), pretifyError('i18n:contributors.deleteFailed')); } diff --git a/frontend/src/app/features/teams/services/team-plans.service.spec.ts b/frontend/src/app/features/teams/services/team-plans.service.spec.ts index 5ac8e576b..9fe44a2cb 100644 --- a/frontend/src/app/features/teams/services/team-plans.service.spec.ts +++ b/frontend/src/app/features/teams/services/team-plans.service.spec.ts @@ -8,11 +8,11 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { ApiUrlConfig, PlanChangedDto, PlanDto, PlansDto, Version } from '@app/shared'; +import { ApiUrlConfig, ChangePlanDto, PlanChangedDto, PlanDto, PlansDto, ReferralInfoDto, Versioned, VersionTag } from '@app/shared'; import { TeamPlansService } from '../internal'; describe('TeamPlansService', () => { - const version = new Version('1'); + const version = new VersionTag('1'); beforeEach(() => { TestBed.configureTestingModule({ @@ -32,8 +32,7 @@ describe('TeamPlansService', () => { it('should make get request to get team plans', inject([TeamPlansService, HttpTestingController], (plansService: TeamPlansService, httpMock: HttpTestingController) => { - let plans: PlansDto; - + let plans: Versioned; plansService.getPlans('my-team').subscribe(result => { plans = result; }); @@ -44,7 +43,7 @@ describe('TeamPlansService', () => { expect(req.request.headers.get('If-Match')).toBeNull(); req.flush({ - currentPlanId: '123', + currentPlanId: 'free', portalLink: 'link/to/portal', planOwner: '456', plans: [ @@ -75,9 +74,7 @@ describe('TeamPlansService', () => { maxContributors: 6500, }, ], - referral: { - code: 'CODE', - }, + referral: { code: 'CODE', earned: '0', condition: 'None' }, locked: 'ManagedByTeam', }, { headers: { @@ -86,45 +83,50 @@ describe('TeamPlansService', () => { }); expect(plans!).toEqual({ - payload: { - currentPlanId: '123', + payload: new PlansDto({ + currentPlanId: 'free', portalLink: 'link/to/portal', planOwner: '456', plans: [ - new PlanDto( - 'free', - 'Free', - '14 €', - 'Change for 14 € per month?', - 'free_yearly', - '120 €', - 'Change for 120 € per year?', - 128, 1000, 1500, 2500), - new PlanDto( - 'professional', - 'Professional', - '18 €', - 'Change for 18 € per month?', - 'professional_yearly', - '160 €', - 'Change for 160 € per year?', - 512, 4000, 5500, 6500), + new PlanDto({ + id: 'free', + name: 'Free', + costs: '14 €', + confirmText: 'Change for 14 € per month?', + yearlyId: 'free_yearly', + yearlyCosts: '120 €', + yearlyConfirmText: 'Change for 120 € per year?', + maxApiBytes: 128, + maxApiCalls: 1000, + maxAssetSize: 1500, + maxContributors: 2500, + }), + new PlanDto({ + id: 'professional', + name: 'Professional', + costs: '18 €', + confirmText: 'Change for 18 € per month?', + yearlyId: 'professional_yearly', + yearlyCosts: '160 €', + yearlyConfirmText: 'Change for 160 € per year?', + maxApiBytes: 512, + maxApiCalls: 4000, + maxAssetSize: 5500, + maxContributors: 6500, + }), ], - referral: { - code: 'CODE', - } as any, + referral: new ReferralInfoDto({ code: 'CODE', earned: '0', condition: 'None' }), locked: 'ManagedByTeam', - }, - version: new Version('2'), + }), + version: new VersionTag('2'), }); })); it('should make put request to change plan', inject([TeamPlansService, HttpTestingController], (plansService: TeamPlansService, httpMock: HttpTestingController) => { - const dto = { planId: 'enterprise' }; + const dto = new ChangePlanDto({ planId: 'enterprise' }); let planChanged: PlanChangedDto; - plansService.putPlan('my-team', dto, version).subscribe(result => { planChanged = result.payload; }); @@ -136,6 +138,6 @@ describe('TeamPlansService', () => { expect(req.request.method).toEqual('PUT'); expect(req.request.headers.get('If-Match')).toBe(version.value); - expect(planChanged!).toEqual({ redirectUri: 'http://url' }); + expect(planChanged!).toEqual(new PlanChangedDto({ redirectUri: 'http://url' })); })); }); diff --git a/frontend/src/app/features/teams/services/team-plans.service.ts b/frontend/src/app/features/teams/services/team-plans.service.ts index 0fdb39192..dbee97306 100644 --- a/frontend/src/app/features/teams/services/team-plans.service.ts +++ b/frontend/src/app/features/teams/services/team-plans.service.ts @@ -8,7 +8,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { ApiUrlConfig, ChangePlanDto, HTTP, mapVersioned, parsePlans, PlanChangedDto, PlansDto, pretifyError, Version, Versioned } from '@app/shared'; +import { ApiUrlConfig, ChangePlanDto, HTTP, mapVersioned, PlanChangedDto, PlansDto, pretifyError, Versioned, VersionOrTag } from '@app/shared'; @Injectable({ providedIn: 'any', @@ -20,22 +20,22 @@ export class TeamPlansService { ) { } - public getPlans(teamId: string): Observable { + public getPlans(teamId: string): Observable> { const url = this.apiUrl.buildUrl(`api/teams/${teamId}/plans`); return HTTP.getVersioned(this.http, url).pipe( mapVersioned(({ body }) => { - return parsePlans(body); + return PlansDto.fromJSON(body); }), pretifyError('i18n:plans.loadFailed')); } - public putPlan(teamId: string, dto: ChangePlanDto, version: Version): Observable> { + public putPlan(teamId: string, dto: ChangePlanDto, version: VersionOrTag): Observable> { const url = this.apiUrl.buildUrl(`api/teams/${teamId}/plan`); - return HTTP.putVersioned(this.http, url, dto, version).pipe( + return HTTP.putVersioned(this.http, url, dto.toJSON(), version).pipe( mapVersioned(({ body }) => { - return body; + return PlanChangedDto.fromJSON(body); }), pretifyError('i18n:plans.changeFailed')); } diff --git a/frontend/src/app/features/teams/state/team-auth.forms.ts b/frontend/src/app/features/teams/state/team-auth.forms.ts index a0d5fed6f..7f174a502 100644 --- a/frontend/src/app/features/teams/state/team-auth.forms.ts +++ b/frontend/src/app/features/teams/state/team-auth.forms.ts @@ -12,7 +12,7 @@ import { map, shareReplay } from 'rxjs/operators'; import { ExtendedFormGroup, Form, value$ } from '@app/framework'; import { AuthSchemeDto } from '@app/shared'; -export class UpdateTeamAuthForm extends Form { +export class UpdateTeamAuthForm extends Form> { public get domain() { return this.form.controls['domain']; } diff --git a/frontend/src/app/features/teams/state/team-auth.state.spec.ts b/frontend/src/app/features/teams/state/team-auth.state.spec.ts index 39d307a41..0033487d9 100644 --- a/frontend/src/app/features/teams/state/team-auth.state.spec.ts +++ b/frontend/src/app/features/teams/state/team-auth.state.spec.ts @@ -8,7 +8,7 @@ import { of, onErrorResumeNextWith, throwError } from 'rxjs'; import { TestValues } from 'src/app/shared/state/_test-helpers'; import { IMock, It, Mock, Times } from 'typemoq'; -import { AuthSchemeDto, DialogService, TeamsService, versioned } from '@app/shared'; +import { AuthSchemeDto, AuthSchemeResponseDto, DialogService, ResourceLinkDto, TeamsService, versioned } from '@app/shared'; import { TeamAuthState } from '../internal'; describe('TeamAuthState', () => { @@ -19,13 +19,13 @@ describe('TeamAuthState', () => { version, } = TestValues; - const scheme: AuthSchemeDto = { + const scheme = new AuthSchemeDto({ domain: 'squidex.io', clientId: 'ID', clientSecret: 'secret', authority: 'Authority', displayName: 'Squidex', - }; + }); let dialogs: IMock; let authService: IMock; @@ -45,13 +45,13 @@ describe('TeamAuthState', () => { describe('Loading', () => { it('should load auth', () => { authService.setup(x => x.getTeamAuth(team)) - .returns(() => of(versioned(version, { scheme, canUpdate: true }))).verifiable(); + .returns(() => of(versioned(version, createAuthResponse(scheme)))).verifiable(); authState.load().subscribe(); expect(authState.snapshot.scheme).toEqual(scheme); - expect(authState.snapshot.canUpdate).toBeTruthy(); expect(authState.snapshot.isLoaded).toBeTruthy(); + expect(authState.snapshot.canUpdate).toBeTruthy(); expect(authState.snapshot.version).toEqual(version); dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); @@ -68,7 +68,7 @@ describe('TeamAuthState', () => { it('should show notification on load if reload is true', () => { authService.setup(x => x.getTeamAuth(team)) - .returns(() => of(versioned(version, { scheme, canUpdate: true }))).verifiable(); + .returns(() => of(versioned(newVersion, createAuthResponse(scheme)))).verifiable(); authState.load(true).subscribe(); @@ -81,35 +81,42 @@ describe('TeamAuthState', () => { describe('Updates', () => { beforeEach(() => { authService.setup(x => x.getTeamAuth(team)) - .returns(() => of(versioned(version, { scheme, canUpdate: true }))).verifiable(); + .returns(() => of(versioned(version, createAuthResponse(scheme)))).verifiable(); authState.load().subscribe(); }); it('should update scheme with new scheme', () => { - const newScheme = { ...scheme, authority: 'NEW AUTHORIY' }; + const newScheme = new AuthSchemeDto({ ...scheme, authority: 'NEW AUTHORIY' }); authService.setup(x => x.putTeamAuth(team, It.isAny(), version)) - .returns(() => of(versioned(newVersion, { scheme: newScheme, canUpdate: true }))); + .returns(() => of(versioned(newVersion, createAuthResponse(newScheme)))).verifiable(); authState.update(newScheme); expect(authState.snapshot.scheme).toEqual(newScheme); - expect(authState.snapshot.canUpdate).toBeTruthy(); expect(authState.snapshot.version).toEqual(newVersion); }); it('should update scheme with deleted scheme', () => { - const newScheme = null; + const newScheme = undefined; authService.setup(x => x.putTeamAuth(team, It.isAny(), version)) - .returns(() => of(versioned(newVersion, { scheme: newScheme, canUpdate: true }))); + .returns(() => of(versioned(newVersion, createAuthResponse(newScheme)))).verifiable(); authState.update(newScheme); expect(authState.snapshot.scheme).toEqual(newScheme); - expect(authState.snapshot.canUpdate).toBeTruthy(); expect(authState.snapshot.version).toEqual(newVersion); }); }); }); + +function createAuthResponse(scheme: AuthSchemeDto | undefined): AuthSchemeResponseDto { + return new AuthSchemeResponseDto({ + scheme, + _links: { + update: new ResourceLinkDto({ method: 'PUT', href: 'teams/42/auth' }), + }, + }); +} diff --git a/frontend/src/app/features/teams/state/team-auth.state.ts b/frontend/src/app/features/teams/state/team-auth.state.ts index 584476a20..57125fb65 100644 --- a/frontend/src/app/features/teams/state/team-auth.state.ts +++ b/frontend/src/app/features/teams/state/team-auth.state.ts @@ -8,7 +8,7 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { finalize, map, tap } from 'rxjs/operators'; -import { AuthSchemeDto, debug, DialogService, LoadingState, shareSubscribed, State, TeamsService, TeamsState, Version } from '@app/shared'; +import { AuthSchemeDto, AuthSchemeValueDto, debug, DialogService, LoadingState, shareSubscribed, State, TeamsService, TeamsState, VersionTag } from '@app/shared'; interface Snapshot extends LoadingState { // The current scheme. @@ -18,7 +18,7 @@ interface Snapshot extends LoadingState { canUpdate?: boolean; // The team version. - version: Version; + version: VersionTag; } @Injectable({ @@ -43,7 +43,7 @@ export class TeamAuthState extends State { private readonly dialogs: DialogService, private readonly teamService: TeamsService, ) { - super({ version: Version.EMPTY }); + super({ version: VersionTag.EMPTY }); debug(this, 'teamAuth'); } @@ -65,10 +65,12 @@ export class TeamAuthState extends State { this.dialogs.notifyInfo('i18n:teams.auth.reloaded'); } + const { scheme, canUpdate } = payload; this.next({ - ...payload, + canUpdate, isLoaded: true, isLoading: false, + scheme, version, }, 'Loading Success'); }), @@ -78,9 +80,9 @@ export class TeamAuthState extends State { shareSubscribed(this.dialogs)); } - public update(scheme: AuthSchemeDto | null): Observable { - return this.teamService.putTeamAuth(this.teamId, { scheme }, this.version).pipe( - tap(({ payload, version }) => { + public update(scheme: AuthSchemeDto | undefined): Observable { + return this.teamService.putTeamAuth(this.teamId, new AuthSchemeValueDto({ scheme }), this.version).pipe( + tap(({ version, payload }) => { this.next({ ...payload, version, diff --git a/frontend/src/app/features/teams/state/team-contributors.forms.ts b/frontend/src/app/features/teams/state/team-contributors.forms.ts index eb712fb2d..8c1668d14 100644 --- a/frontend/src/app/features/teams/state/team-contributors.forms.ts +++ b/frontend/src/app/features/teams/state/team-contributors.forms.ts @@ -9,7 +9,7 @@ import { UntypedFormControl, Validators } from '@angular/forms'; import { debounceTime, map, shareReplay } from 'rxjs/operators'; import { AssignContributorDto, ExtendedFormGroup, Form, hasNoValue$, Types, UserDto, value$ } from '@app/shared'; -export class AssignTeamContributorForm extends Form { +export class AssignTeamContributorForm extends Form { public get user() { return this.form.controls['user']; } @@ -31,13 +31,11 @@ export class AssignTeamContributorForm extends Form; - -export class ImportContributorsForm extends Form { +export class ImportContributorsForm extends Form> { public get import() { return this.form.controls['import']; } @@ -66,12 +64,10 @@ function extractEmails(value: string) { const added: { [email: string]: boolean } = {}; const emails = value.match(EMAIL_REGEX); - if (emails) { for (const match of emails) { if (!added[match]) { - result.push({ contributorId: match, role: 'Owner', invite: true }); - + result.push(new AssignContributorDto({ contributorId: match, role: 'Owner', invite: true })); added[match] = true; } } diff --git a/frontend/src/app/features/teams/state/team-contributors.state.spec.ts b/frontend/src/app/features/teams/state/team-contributors.state.spec.ts index 6bcef4f26..b05167310 100644 --- a/frontend/src/app/features/teams/state/team-contributors.state.spec.ts +++ b/frontend/src/app/features/teams/state/team-contributors.state.spec.ts @@ -10,7 +10,7 @@ import { catchError } from 'rxjs/operators'; import { createContributors } from 'src/app/shared/services/contributors.service.spec'; import { TestValues } from 'src/app/shared/state/_test-helpers'; import { IMock, It, Mock, Times } from 'typemoq'; -import { ContributorDto, ContributorsPayload, DialogService, ErrorDto, versioned } from '@app/shared'; +import { AssignContributorDto, ContributorDto, ContributorsDto, DialogService, ErrorDto, versioned } from '@app/shared'; import { TeamContributorsService, TeamContributorsState } from '../internal'; describe('TeamContributorsState', () => { @@ -138,7 +138,7 @@ describe('TeamContributorsState', () => { it('should update contributors if user assigned', () => { const updated = createContributors(5, 6); - const request = { contributorId: 'mail2stehle@gmail.com', role: 'Developer' }; + const request = new AssignContributorDto({ contributorId: 'mail2stehle@gmail.com', role: 'Developer' }); contributorsService.setup(x => x.postContributor(team, request, version)) .returns(() => of(versioned(newVersion, updated))).verifiable(); @@ -149,7 +149,7 @@ describe('TeamContributorsState', () => { }); it('should return proper error if user to add does not exist', () => { - const request = { contributorId: 'mail2stehle@gmail.com', role: 'Developer' }; + const request = new AssignContributorDto({ contributorId: 'mail2stehle@gmail.com', role: 'Developer' }); contributorsService.setup(x => x.postContributor(team, request, version)) .returns(() => throwError(() => new ErrorDto(404, '404'))); @@ -168,7 +168,7 @@ describe('TeamContributorsState', () => { }); it('should return original error if not a 404', () => { - const request = { contributorId: 'mail2stehle@gmail.com', role: 'Developer' }; + const request = new AssignContributorDto({ contributorId: 'mail2stehle@gmail.com', role: 'Developer' }); contributorsService.setup(x => x.postContributor(team, request, version)) .returns(() => throwError(() => new ErrorDto(500, '500'))); @@ -197,7 +197,7 @@ describe('TeamContributorsState', () => { expectNewContributors(updated); }); - function expectNewContributors(updated: ContributorsPayload) { + function expectNewContributors(updated: ContributorsDto) { expect(contributorsState.snapshot.contributors).toEqual(updated.items); expect(contributorsState.snapshot.maxContributors).toBe(updated.maxContributors); expect(contributorsState.snapshot.version).toEqual(newVersion); diff --git a/frontend/src/app/features/teams/state/team-contributors.state.ts b/frontend/src/app/features/teams/state/team-contributors.state.ts index 8912334be..900fc9963 100644 --- a/frontend/src/app/features/teams/state/team-contributors.state.ts +++ b/frontend/src/app/features/teams/state/team-contributors.state.ts @@ -8,7 +8,7 @@ import { Injectable } from '@angular/core'; import { EMPTY, Observable, throwError } from 'rxjs'; import { catchError, finalize, tap } from 'rxjs/operators'; -import { AssignContributorDto, ContributorDto, ContributorsPayload, debug, DialogService, ErrorDto, getPagingInfo, ListState, shareMapSubscribed, shareSubscribed, State, TeamsState, Types, Version } from '@app/shared'; +import { AssignContributorDto, ContributorDto, ContributorsDto, debug, DialogService, ErrorDto, getPagingInfo, ListState, shareMapSubscribed, shareSubscribed, State, TeamsState, Types, VersionTag } from '@app/shared'; import { TeamContributorsService } from '../internal'; interface Snapshot extends ListState { @@ -19,7 +19,7 @@ interface Snapshot extends ListState { maxContributors: number; // The team version. - version: Version; + version: VersionTag; // Indicates if the user can add a contributor. canCreate?: boolean; @@ -71,7 +71,7 @@ export class TeamContributorsState extends State { page: 0, pageSize: 10, total: 0, - version: Version.EMPTY, + version: VersionTag.EMPTY, }); debug(this, 'teamContributors'); @@ -141,7 +141,9 @@ export class TeamContributorsState extends State { shareMapSubscribed(this.dialogs, x => x.payload.isInvited, options)); } - private replaceContributors(version: Version, { canCreate, items, maxContributors }: ContributorsPayload) { + private replaceContributors(version: VersionTag, payload: ContributorsDto) { + const { canCreate, items, maxContributors } = payload; + this.next({ canCreate, contributors: items, diff --git a/frontend/src/app/features/teams/state/team-plans.state.spec.ts b/frontend/src/app/features/teams/state/team-plans.state.spec.ts index 5d47c0444..86de329de 100644 --- a/frontend/src/app/features/teams/state/team-plans.state.spec.ts +++ b/frontend/src/app/features/teams/state/team-plans.state.spec.ts @@ -8,7 +8,7 @@ import { of, onErrorResumeNextWith, throwError } from 'rxjs'; import { TestValues } from 'src/app/shared/state/_test-helpers'; import { IMock, It, Mock, Times } from 'typemoq'; -import { DialogService, PlanDto, PlanLockedReason, versioned } from '@app/shared'; +import { DialogService, PlanChangedDto, PlanDto, PlansDto, versioned } from '@app/shared'; import { TeamPlansService, TeamPlansState } from '../internal'; describe('TeamPlansState', () => { @@ -20,15 +20,39 @@ describe('TeamPlansState', () => { version, } = TestValues; - const oldPlans = { - currentPlanId: 'id1', + const oldPlans = new PlansDto({ + currentPlanId: 'free', planOwner: creator, plans: [ - new PlanDto('id1', 'name1', '100€', undefined, 'id1_yearly', '200€', undefined, 1, 1, 1, 1), - new PlanDto('id2', 'name2', '400€', undefined, 'id2_yearly', '800€', undefined, 2, 2, 2, 2), + new PlanDto({ + id: 'free', + name: 'Free', + costs: '14 €', + confirmText: 'Change for 14 € per month?', + yearlyId: 'free_yearly', + yearlyCosts: '120 €', + yearlyConfirmText: 'Change for 120 € per year?', + maxApiBytes: 128, + maxApiCalls: 1000, + maxAssetSize: 1500, + maxContributors: 2500, + }), + new PlanDto({ + id: 'professional', + name: 'Professional', + costs: '18 €', + confirmText: 'Change for 18 € per month?', + yearlyId: 'professional_yearly', + yearlyCosts: '160 €', + yearlyConfirmText: 'Change for 160 € per year?', + maxApiBytes: 512, + maxApiCalls: 4000, + maxAssetSize: 5500, + maxContributors: 6500, + }), ], - locked: 'None' as PlanLockedReason, - }; + locked: 'None', + }); let dialogs: IMock; let plansService: IMock; @@ -66,7 +90,7 @@ describe('TeamPlansState', () => { plansService.setup(x => x.getPlans(team)) .returns(() => of(versioned(version, oldPlans))).verifiable(); - plansState.load(false, 'id2_yearly').subscribe(); + plansState.load(false, 'professional_yearly').subscribe(); expect(plansState.snapshot.plans).toEqual([ { isSelected: false, isYearlySelected: false, plan: oldPlans.plans[0] }, @@ -116,7 +140,7 @@ describe('TeamPlansState', () => { const result = { redirectUri: 'http://url' }; plansService.setup(x => x.putPlan(team, It.isAny(), version)) - .returns(() => of(versioned(newVersion, result))); + .returns(() => of(versioned(newVersion, new PlanChangedDto(result)))); plansState.change('free').pipe(onErrorResumeNextWith()).subscribe(); @@ -128,11 +152,11 @@ describe('TeamPlansState', () => { expect(plansState.snapshot.version).toEqual(version); }); - it('should update plans if no returning url', () => { + it('should update plans if not returning url', () => { plansService.setup(x => x.putPlan(team, It.isAny(), version)) - .returns(() => of(versioned(newVersion, { redirectUri: '' }))); + .returns(() => of(versioned(newVersion, new PlanChangedDto()))); - plansState.change('id2_yearly').pipe(onErrorResumeNextWith()).subscribe(); + plansState.change('professional_yearly').pipe(onErrorResumeNextWith()).subscribe(); expect(plansState.snapshot.plans).toEqual([ { isSelected: false, isYearlySelected: false, plan: oldPlans.plans[0] }, diff --git a/frontend/src/app/features/teams/state/team-plans.state.ts b/frontend/src/app/features/teams/state/team-plans.state.ts index d69a310de..f826ab23c 100644 --- a/frontend/src/app/features/teams/state/team-plans.state.ts +++ b/frontend/src/app/features/teams/state/team-plans.state.ts @@ -8,7 +8,7 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { finalize, tap } from 'rxjs/operators'; -import { debug, DialogService, LoadingState, PlanDto, PlanLockedReason, ReferralDto, shareSubscribed, State, TeamsState, Version } from '@app/shared'; +import { ChangePlanDto, debug, DialogService, LoadingState, PlanDto, PlansLockedReason, ReferralInfoDto, shareSubscribed, State, TeamsState, VersionTag } from '@app/shared'; import { TeamPlansService } from '../internal'; export interface PlanInfo { @@ -33,13 +33,13 @@ interface Snapshot extends LoadingState { portalLink?: string; // The referral info. - referral?: ReferralDto; + referral?: ReferralInfoDto; // The reason why the plan cannot be changed. - locked?: PlanLockedReason; + locked?: PlansLockedReason; // The team version. - version: Version; + version: VersionTag; } @Injectable({ @@ -78,7 +78,7 @@ export class TeamPlansState extends State { private readonly dialogs: DialogService, private readonly plansService: TeamPlansService, ) { - super({ plans: [], version: Version.EMPTY }); + super({ plans: [], version: VersionTag.EMPTY }); debug(this, 'teamPlans'); } @@ -100,7 +100,7 @@ export class TeamPlansState extends State { this.dialogs.notifyInfo('i18n:plans.reloaded'); } - const planId = overridePlanId || payload.currentPlanId; + const planId = overridePlanId || payload.currentPlanId!; const plans = payload.plans.map(x => createPlan(x, planId)); this.next({ @@ -121,8 +121,8 @@ export class TeamPlansState extends State { } public change(planId: string): Observable { - return this.plansService.putPlan(this.teamId, { planId }, this.version).pipe( - tap(({ payload, version }) => { + return this.plansService.putPlan(this.teamId, new ChangePlanDto({ planId }), this.version).pipe( + tap(({ version, payload }) => { if (payload.redirectUri && payload.redirectUri.length > 0) { this.window.location.href = payload.redirectUri; } else { diff --git a/frontend/src/app/framework/angular/forms/editable-title.component.ts b/frontend/src/app/framework/angular/forms/editable-title.component.ts index 8c8af2de5..08d16db37 100644 --- a/frontend/src/app/framework/angular/forms/editable-title.component.ts +++ b/frontend/src/app/framework/angular/forms/editable-title.component.ts @@ -31,7 +31,7 @@ export class EditableTitleComponent { public inputTitleChange = new EventEmitter(); @Input({ required: true }) - public inputTitle!: string; + public inputTitle?: string; @Input({ transform: numberAttribute }) public inputTitleLength = 20; diff --git a/frontend/src/app/framework/angular/http/http-extensions.ts b/frontend/src/app/framework/angular/http/http-extensions.ts index dd62637b0..2fc5bb66d 100644 --- a/frontend/src/app/framework/angular/http/http-extensions.ts +++ b/frontend/src/app/framework/angular/http/http-extensions.ts @@ -9,54 +9,55 @@ import { HttpClient, HttpErrorResponse, HttpEvent, HttpHeaders, HttpRequest, HttpResponse } from '@angular/common/http'; import { catchError, map, Observable, throwError } from 'rxjs'; -import { ErrorDto, Types, Version, Versioned } from '@app/framework/internal'; +import { ErrorDto, getActualVersion, Types, Versioned, VersionOrTag, VersionTag } from '@app/framework/internal'; export module HTTP { export type UploadFile = File | { url: string; name: string }; - export function upload(http: HttpClient, method: string, url: string, file: UploadFile, version?: Version): Observable> { + export function upload(http: HttpClient, method: string, url: string, file: UploadFile, + version?: VersionOrTag): Observable> { const req = new HttpRequest(method, url, getFormData(file), { headers: createHeaders(version, undefined), reportProgress: true }); return http.request(req); } export function getVersioned(http: HttpClient, url: string, - version?: Version, customHeaders?: HttpHeaders): Observable>> { + version?: VersionOrTag, customHeaders?: HttpHeaders): Observable>> { const headers = createHeaders(version, customHeaders); return handleVersion(http.get(url, { observe: 'response', headers })); } export function postVersioned(http: HttpClient, url: string, body: any, - version?: Version, customHeaders?: HttpHeaders): Observable>> { + version?: VersionOrTag, customHeaders?: HttpHeaders): Observable>> { const headers = createHeaders(version, customHeaders); return handleVersion(http.post(url, body, { observe: 'response', headers })); } export function putVersioned(http: HttpClient, url: string, body: any, - version?: Version, customHeaders?: HttpHeaders): Observable>> { + version?: VersionOrTag, customHeaders?: HttpHeaders): Observable>> { const headers = createHeaders(version, customHeaders); return handleVersion(http.put(url, body, { observe: 'response', headers })); } export function patchVersioned(http: HttpClient, url: string, body: any, - version?: Version, customHeaders?: HttpHeaders): Observable>> { + version?: VersionOrTag, customHeaders?: HttpHeaders): Observable>> { const headers = createHeaders(version, customHeaders); return handleVersion(http.request('PATCH', url, { body, observe: 'response', headers })); } export function deleteVersioned(http: HttpClient, url: string, - version?: Version, customHeaders?: HttpHeaders): Observable>> { + version?: VersionOrTag, customHeaders?: HttpHeaders): Observable>> { const headers = createHeaders(version, customHeaders); return handleVersion(http.delete(url, { observe: 'response', headers })); } export function requestVersioned(http: HttpClient, method: string, url: string, - version?: Version, body?: any, customHeaders?: HttpHeaders): Observable>> { + version?: VersionOrTag, body?: any, customHeaders?: HttpHeaders): Observable>> { const headers = createHeaders(version, customHeaders); return handleVersion(http.request(method, url, { observe: 'response', headers, body })); @@ -76,11 +77,12 @@ export module HTTP { return formData; } - function createHeaders(version: Version | undefined, customHeaders: HttpHeaders | undefined): HttpHeaders { + function createHeaders(version: VersionOrTag | undefined, customHeaders: HttpHeaders | undefined): HttpHeaders { customHeaders ||= new HttpHeaders(); - if (version && version.value && version.value.length > 0) { - return customHeaders.set('If-Match', version.value); + const actualVersion = getActualVersion(version); + if (actualVersion) { + return customHeaders.set('If-Match', `${actualVersion}`); } return customHeaders; @@ -90,7 +92,7 @@ export module HTTP { return httpRequest.pipe(map((response: HttpResponse) => { const etag = response.headers.get('etag') || ''; - return { version: new Version(etag), payload: response }; + return { version: new VersionTag(etag), payload: response }; })); } } diff --git a/frontend/src/app/framework/angular/long-hover.directive.stories.ts b/frontend/src/app/framework/angular/long-hover.directive.stories.ts index 457950254..23737609e 100644 --- a/frontend/src/app/framework/angular/long-hover.directive.stories.ts +++ b/frontend/src/app/framework/angular/long-hover.directive.stories.ts @@ -45,16 +45,16 @@ type Story = StoryObj; export const Default: Story = { args: { - hover: action('Hover') as any, + hover: action('Hover'), selector: '', - cancelled: action('Cancelled') as any, + cancelled: action('Cancelled'), }, }; export const Selector: Story = { args: { - hover: action('Hover') as any, + hover: action('Hover'), selector: 'button', - cancelled: action('Cancelled') as any, + cancelled: action('Cancelled'), }, }; \ No newline at end of file diff --git a/frontend/src/app/framework/angular/markdown.directive.spec.ts b/frontend/src/app/framework/angular/markdown.directive.spec.ts index 5deeca4a0..ed470b985 100644 --- a/frontend/src/app/framework/angular/markdown.directive.spec.ts +++ b/frontend/src/app/framework/angular/markdown.directive.spec.ts @@ -11,14 +11,14 @@ import { MarkdownDirective } from './markdown.directive'; describe('MarkdownDirective', () => { let renderer: IMock; - let markdownElement = {}; + let markdownElement: any = {}; let markdownDirective: MarkdownDirective; beforeEach(() => { renderer = Mock.ofType(); markdownElement = {}; - markdownDirective = new MarkdownDirective(markdownElement as any, renderer.object); + markdownDirective = new MarkdownDirective(markdownElement, renderer.object); }); it('should render empty text as text', () => { diff --git a/frontend/src/app/framework/angular/markdown.directive.ts b/frontend/src/app/framework/angular/markdown.directive.ts index 46f4a8699..94f709d00 100644 --- a/frontend/src/app/framework/angular/markdown.directive.ts +++ b/frontend/src/app/framework/angular/markdown.directive.ts @@ -14,7 +14,7 @@ import { markdownRender } from '@app/framework/internal'; }) export class MarkdownDirective { @Input('sqxMarkdown') - public markdown!: string; + public markdown!: string | undefined; @Input({ transform: booleanAttribute }) public trusted = false; @@ -35,19 +35,20 @@ export class MarkdownDirective { let html = ''; let markdown = this.markdown; - - const hasExclamation = markdown.indexOf('!') === 0; - - if (hasExclamation && this.optional) { - markdown = markdown.substring(1); - } - - if (!markdown) { - html = markdown; - } else if (this.optional && !hasExclamation) { - html = markdown; - } else if (this.markdown) { - html = markdownRender(markdown, this.inline, this.trusted); + if (markdown) { + const hasExclamation = markdown.indexOf('!') === 0; + + if (hasExclamation && this.optional) { + markdown = markdown.substring(1); + } + + if (!markdown) { + html = markdown; + } else if (this.optional && !hasExclamation) { + html = markdown; + } else if (this.markdown) { + html = markdownRender(markdown, this.inline, this.trusted); + } } const hasHtml = html.indexOf('<') >= 0 || html.indexOf('&') >= 0; diff --git a/frontend/src/app/framework/angular/pipes/keys.pipe.spec.ts b/frontend/src/app/framework/angular/pipes/keys.pipe.spec.ts index 4c5f1688f..15dbbb691 100644 --- a/frontend/src/app/framework/angular/pipes/keys.pipe.spec.ts +++ b/frontend/src/app/framework/angular/pipes/keys.pipe.spec.ts @@ -21,4 +21,16 @@ describe('KeysPipe', () => { expect(actual).toEqual(expected); }); + + it('should return sorted keys', () => { + const value = { + key2: 2, + key1: 1, + }; + + const actual = pipe.transform(value); + const expected = ['key1', 'key2']; + + expect(actual).toEqual(expected); + }); }); diff --git a/frontend/src/app/framework/angular/pipes/keys.pipe.ts b/frontend/src/app/framework/angular/pipes/keys.pipe.ts index 8af522755..3d1549c56 100644 --- a/frontend/src/app/framework/angular/pipes/keys.pipe.ts +++ b/frontend/src/app/framework/angular/pipes/keys.pipe.ts @@ -14,6 +14,6 @@ import { Pipe, PipeTransform } from '@angular/core'; }) export class KeysPipe implements PipeTransform { public transform(value: any): any { - return Object.keys(value); + return Object.keys(value).sorted(); } } diff --git a/frontend/src/app/framework/services/localizer.service.ts b/frontend/src/app/framework/services/localizer.service.ts index d71393bd8..84bf29066 100644 --- a/frontend/src/app/framework/services/localizer.service.ts +++ b/frontend/src/app/framework/services/localizer.service.ts @@ -29,7 +29,6 @@ export class LocalizerService { } let text = (this.translations as any)[key]; - if (!text) { return null; } diff --git a/frontend/src/app/framework/services/title.service.ts b/frontend/src/app/framework/services/title.service.ts index 526b213b6..ce528863d 100644 --- a/frontend/src/app/framework/services/title.service.ts +++ b/frontend/src/app/framework/services/title.service.ts @@ -48,29 +48,28 @@ export class TitleService { public push(value: string, index?: number, route?: any) { let result: number | undefined; + if (!value) { + return result; + } - if (value) { - const clone = [...this.path$.value]; + const clone = [...this.path$.value]; - const lastIndex = clone.length - 1; - const localized = this.localizer.getOrKey(value); + const lastIndex = clone.length - 1; + const localized = this.localizer.getOrKey(value); - const title = { localized, value, route }; + const title = { localized, value, route }; - if (Types.isNumber(index) && index >= 0 && index <= lastIndex) { - clone[index] = title; + if (Types.isNumber(index) && index >= 0 && index <= lastIndex) { + clone[index] = title; - result = index; - } else { - clone.push(title); + result = index; + } else { + clone.push(title); - result = lastIndex + 1; - } - - this.path$.next(clone); + result = lastIndex + 1; } - return result; + this.path$.next(clone); } public pop() { diff --git a/frontend/src/app/framework/utils/hateos.ts b/frontend/src/app/framework/utils/hateos.ts index 9be8d359d..acecd0df4 100644 --- a/frontend/src/app/framework/utils/hateos.ts +++ b/frontend/src/app/framework/utils/hateos.ts @@ -8,11 +8,11 @@ export interface Resource { _links: ResourceLinks; - _meta?: Metadata; + _meta?: Metadata | any; } export type ResourceLinks = { [rel: string]: ResourceLink }; -export type ResourceLink = { href: string; method: ResourceMethod; metadata?: string }; +export type ResourceLink = { href: string; method: ResourceMethod | string; metadata?: string }; export type Metadata = { [rel: string]: string }; diff --git a/frontend/src/app/framework/utils/rxjs-extensions.ts b/frontend/src/app/framework/utils/rxjs-extensions.ts index fcb1fa840..87659481c 100644 --- a/frontend/src/app/framework/utils/rxjs-extensions.ts +++ b/frontend/src/app/framework/utils/rxjs-extensions.ts @@ -8,9 +8,9 @@ import { EMPTY, Observable, of, onErrorResumeNextWith, ReplaySubject, throwError } from 'rxjs'; import { catchError, debounceTime, distinctUntilChanged, filter, map, share, switchMap, tap } from 'rxjs/operators'; import { DialogService } from '../services/dialog.service'; -import { Version, versioned, Versioned } from './version'; +import { versioned, Versioned, VersionTag } from './version'; -export function mapVersioned(project: (value: T, version: Version) => R) { +export function mapVersioned(project: (value: T, version: VersionTag) => R) { return function mapOperation(source: Observable>) { return source.pipe(map, Versioned>(({ version, payload }) => { return versioned(version, project(payload, version)); diff --git a/frontend/src/app/framework/utils/version.spec.ts b/frontend/src/app/framework/utils/version.spec.ts index 7ca6f002f..4ddc06a9f 100644 --- a/frontend/src/app/framework/utils/version.spec.ts +++ b/frontend/src/app/framework/utils/version.spec.ts @@ -5,19 +5,19 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Version } from './version'; +import { VersionTag } from './version'; describe('Version', () => { it('should initialize with init value', () => { - const version = new Version('1.0'); + const version = new VersionTag('1.0'); expect(version.value).toBe('1.0'); }); it('should ignore prefix for equal comparison', () => { - expect(new Version('2').eq(new Version('2'))).toBeTruthy(); - expect(new Version('2').eq(new Version('W/2'))).toBeTruthy(); - expect(new Version('W/2').eq(new Version('2'))).toBeTruthy(); - expect(new Version('W/2').eq(new Version('W/2'))).toBeTruthy(); + expect(new VersionTag('2').eq(new VersionTag('2'))).toBeTruthy(); + expect(new VersionTag('2').eq(new VersionTag('W/2'))).toBeTruthy(); + expect(new VersionTag('W/2').eq(new VersionTag('2'))).toBeTruthy(); + expect(new VersionTag('W/2').eq(new VersionTag('W/2'))).toBeTruthy(); }); }); diff --git a/frontend/src/app/framework/utils/version.ts b/frontend/src/app/framework/utils/version.ts index ec1b75d4c..747c30211 100644 --- a/frontend/src/app/framework/utils/version.ts +++ b/frontend/src/app/framework/utils/version.ts @@ -5,15 +5,17 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -export class Version { - public static readonly EMPTY = new Version(''); +import { Types } from './types'; + +export class VersionTag { + public static readonly EMPTY = new VersionTag(''); constructor( public readonly value: string, ) { } - public eq(other: Version) { + public eq(other: VersionTag) { return other && other.trimmed() === this.trimmed(); } @@ -30,8 +32,19 @@ export class Version { } } -export function versioned(version: Version, payload: T = undefined!): Versioned { +export type Version = number; +export type VersionOrTag = VersionTag | Version; + +export function getActualVersion(source: VersionOrTag | undefined): string | number | undefined { + if (Types.is(source, VersionTag)) { + return source.value; + } else { + return source; + } +} + +export function versioned(version: VersionTag, payload: T = undefined!): Versioned { return { version, payload }; } -export type Versioned = Readonly<{ version: Version; payload: T }>; +export type Versioned = Readonly<{ version: VersionTag; payload: T }>; diff --git a/frontend/src/app/shared/components/app-form.component.ts b/frontend/src/app/shared/components/app-form.component.ts index 9dc2e5cd1..d4ab1f98b 100644 --- a/frontend/src/app/shared/components/app-form.component.ts +++ b/frontend/src/app/shared/components/app-form.component.ts @@ -9,7 +9,7 @@ import { AsyncPipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ApiUrlConfig, ControlErrorsComponent, FocusOnInitDirective, FormAlertComponent, FormErrorComponent, FormHintComponent, ModalDialogComponent, TooltipDirective, TransformInputDirective, TranslatePipe } from '@app/framework'; -import { AppsState, CreateAppForm, TemplateDto } from '@app/shared/internal'; +import { AppsState, CreateAppDto, CreateAppForm, TemplateDto } from '@app/shared/internal'; @Component({ standalone: true, @@ -53,19 +53,20 @@ export class AppFormComponent { public createApp() { const value = this.createForm.submit(); + if (!value) { + return; + } - if (value) { - const request = { ...value, template: this.template?.name }; + const request = new CreateAppDto({ ...value, template: this.template?.name }); - this.appsStore.create(request) - .subscribe({ - next: () => { - this.emitClose(); - }, - error: error => { - this.createForm.submitFailed(error); - }, - }); - } + this.appsStore.create(request) + .subscribe({ + next: () => { + this.emitClose(); + }, + error: error => { + this.createForm.submitFailed(error); + }, + }); } } diff --git a/frontend/src/app/shared/components/assets/asset-dialog.component.ts b/frontend/src/app/shared/components/assets/asset-dialog.component.ts index ca091f60a..4ad1a80a5 100644 --- a/frontend/src/app/shared/components/assets/asset-dialog.component.ts +++ b/frontend/src/app/shared/components/assets/asset-dialog.component.ts @@ -13,7 +13,7 @@ import { NgxDocViewerModule } from 'ngx-doc-viewer'; import { BehaviorSubject, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { ConfirmClickDirective, ControlErrorsComponent, CopyDirective, DialogService, FormErrorComponent, FormHintComponent, HTTP, ModalDialogComponent, ProgressBarComponent, switchMapCached, TagEditorComponent, TooltipDirective, TransformInputDirective, TranslatePipe, Types, VideoPlayerComponent } from '@app/framework'; -import { AnnotateAssetDto, AnnotateAssetForm, AppsState, AssetDto, AssetPathItem, AssetsService, AssetsState, AssetUploaderState, AuthService, MoveAssetForm, MoveAssetItemDto, ROOT_ITEM, UploadCanceled } from '@app/shared/internal'; +import { AnnotateAssetForm, AppsState, AssetDto, AssetPathItem, AssetsService, AssetsState, AssetUploaderState, AuthService, AnnotateAssetDto, MoveAssetDto, MoveAssetForm, ROOT_ITEM, UploadCanceled } from '@app/shared/internal'; import { AssetFolderDropdownComponent } from './asset-folder-dropdown.component'; import { AssetHistoryComponent } from './asset-history.component'; import { AssetPathComponent } from './asset-path.component'; @@ -258,7 +258,7 @@ export class AssetDialogComponent implements OnInit { }); } - private moveInternal(values: MoveAssetItemDto | null) { + private moveInternal(values: MoveAssetDto | null) { if (!values) { this.isMoving = false; return; diff --git a/frontend/src/app/shared/components/assets/image-focus-point.component.ts b/frontend/src/app/shared/components/assets/image-focus-point.component.ts index d95ac9c46..c0fc298e6 100644 --- a/frontend/src/app/shared/components/assets/image-focus-point.component.ts +++ b/frontend/src/app/shared/components/assets/image-focus-point.component.ts @@ -86,9 +86,13 @@ export class ImageFocusPointComponent implements AfterViewInit, OnDestroy { return null; } - const metadata = { ...asset.metadata, focusX: this.x, focusY: this.y }; - - return { metadata }; + return new AnnotateAssetDto({ + metadata: { + ...asset.metadata, + focusX: this.x, + focusY: this.y, + }, + }); } } diff --git a/frontend/src/app/shared/components/assets/pipes.ts b/frontend/src/app/shared/components/assets/pipes.ts index ba67c8ec3..ca0d3fe50 100644 --- a/frontend/src/app/shared/components/assets/pipes.ts +++ b/frontend/src/app/shared/components/assets/pipes.ts @@ -6,7 +6,7 @@ */ import { Pipe, PipeTransform } from '@angular/core'; -import { Version } from '@app/framework'; +import { VersionOrTag, VersionTag } from '@app/framework'; import { ApiUrlConfig, AssetDto, AuthService, MathHelper, StringHelper, Types } from '@app/shared/internal'; @Pipe({ @@ -20,7 +20,7 @@ export class AssetUrlPipe implements PipeTransform { ) { } - public transform(asset: AssetDto, version?: number | Version, withQuery = false): string { + public transform(asset: AssetDto, version?: VersionOrTag, withQuery = false): string { const url = asset.fullUrl(this.apiUrl); const query: Record = {}; @@ -30,7 +30,7 @@ export class AssetUrlPipe implements PipeTransform { if (Types.isNumber(version)) { query['version'] = version; - } else if (Types.is(version, Version)) { + } else if (Types.is(version, VersionTag)) { query['version'] = version.value; } diff --git a/frontend/src/app/shared/components/contents/content-list-field.component.ts b/frontend/src/app/shared/components/contents/content-list-field.component.ts index dc421e2c3..022e61cef 100644 --- a/frontend/src/app/shared/components/contents/content-list-field.component.ts +++ b/frontend/src/app/shared/components/contents/content-list-field.component.ts @@ -9,7 +9,7 @@ import { booleanAttribute, ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { UntypedFormGroup } from '@angular/forms'; import { FromNowPipe, ShortDatePipe, TooltipDirective, TranslatePipe } from '@app/framework'; -import { ContentDto, FieldValue, getContentValue, LanguageDto, META_FIELDS, SchemaDto, StatefulComponent, TableField, TableSettings } from '@app/shared/internal'; +import { AppLanguageDto, ContentDto, FieldValue, getContentValue, META_FIELDS, SchemaDto, StatefulComponent, TableField, TableSettings } from '@app/shared/internal'; import { UserNameRefPipe, UserPictureRefPipe } from '../pipes'; import { ContentStatusComponent } from './content-status.component'; import { ContentValueEditorComponent } from './content-value-editor.component'; @@ -62,10 +62,10 @@ export class ContentListFieldComponent extends StatefulComponent { public schema?: SchemaDto; @Input({ required: true }) - public language!: LanguageDto; + public language!: AppLanguageDto; @Input({ required: true }) - public languages!: ReadonlyArray; + public languages!: ReadonlyArray; public get isInlineEditable() { return this.field.rootField?.isInlineEditable === true; diff --git a/frontend/src/app/shared/components/contents/content-list-header.component.ts b/frontend/src/app/shared/components/contents/content-list-header.component.ts index 51696104d..ca896c81c 100644 --- a/frontend/src/app/shared/components/contents/content-list-header.component.ts +++ b/frontend/src/app/shared/components/contents/content-list-header.component.ts @@ -6,7 +6,7 @@ */ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; -import { LanguageDto, META_FIELDS, Query, SortMode, TableField } from '@app/shared/internal'; +import { AppLanguageDto, META_FIELDS, Query, SortMode, TableField } from '@app/shared/internal'; import { TableHeaderComponent } from '../table-header.component'; @Component({ @@ -32,7 +32,7 @@ export class ContentListHeaderComponent { public query: Query | undefined; @Input({ required: true }) - public language!: LanguageDto; + public language!: AppLanguageDto; public sortPath?: string; public sortDefault?: SortMode; diff --git a/frontend/src/app/shared/components/contents/content-status.component.ts b/frontend/src/app/shared/components/contents/content-status.component.ts index 926a1d33e..9960cfd58 100644 --- a/frontend/src/app/shared/components/contents/content-status.component.ts +++ b/frontend/src/app/shared/components/contents/content-status.component.ts @@ -8,7 +8,7 @@ import { booleanAttribute, ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { FullDateTimePipe, LocalizerService, TooltipDirective, TranslatePipe, TypedSimpleChanges } from '@app/framework'; -import { ScheduleDto } from '@app/shared/internal'; +import { ScheduleJobDto } from '@app/shared/internal'; @Component({ standalone: true, @@ -30,7 +30,7 @@ export class ContentStatusComponent { public statusColor!: string; @Input() - public scheduled?: ScheduleDto | null; + public scheduled?: ScheduleJobDto | null; @Input() public layout: 'icon' | 'text' | 'multiline' = 'icon'; diff --git a/frontend/src/app/shared/components/contents/translation-status.component.ts b/frontend/src/app/shared/components/contents/translation-status.component.ts index dbb480334..c6e4f43fc 100644 --- a/frontend/src/app/shared/components/contents/translation-status.component.ts +++ b/frontend/src/app/shared/components/contents/translation-status.component.ts @@ -6,7 +6,7 @@ */ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; -import { contentTranslationStatus, LanguageDto, SchemaDto } from '@app/shared/internal'; +import { AppLanguageDto, contentTranslationStatus, SchemaDto } from '@app/shared/internal'; @Component({ standalone: true, @@ -20,10 +20,10 @@ export class TranslationStatusComponent { public data!: any; @Input() - public language?: LanguageDto | null; + public language?: AppLanguageDto | null; @Input({ required: true }) - public languages!: ReadonlyArray; + public languages!: ReadonlyArray; @Input({ required: true }) public schema?: SchemaDto; 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 5ea04520a..ab71b91c1 100644 --- a/frontend/src/app/shared/components/forms/rich-editor.component.ts +++ b/frontend/src/app/shared/components/forms/rich-editor.component.ts @@ -10,7 +10,7 @@ import { AfterViewInit, booleanAttribute, ChangeDetectionStrategy, Component, El import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { BehaviorSubject, catchError, of, switchMap } from 'rxjs'; import { HTTP, ModalDirective, TypedSimpleChanges } from '@app/framework'; -import { ApiUrlConfig, AppsState, AssetDto, AssetsService, AssetUploaderState, ContentDto, DialogModel, getContentValue, LanguageDto, ResourceLoaderService, StatefulControlComponent, Types } from '@app/shared/internal'; +import { ApiUrlConfig, AppLanguageDto, AppsState, AssetDto, AssetsService, AssetUploaderState, ContentDto, DialogModel, getContentValue, ResourceLoaderService, StatefulControlComponent, Types } from '@app/shared/internal'; import { AssetDialogComponent } from '../assets/asset-dialog.component'; import { AssetSelectorComponent } from '../assets/asset-selector.component'; import { ChatDialogComponent } from '../chat-dialog.component'; @@ -71,10 +71,10 @@ export class RichEditorComponent extends StatefulControlComponent<{}, EditorValu public schemaIds?: ReadonlyArray; @Input() - public language!: LanguageDto; + public language!: AppLanguageDto; @Input() - public languages!: ReadonlyArray; + public languages!: ReadonlyArray; @Input() public folderId = ''; @@ -295,7 +295,7 @@ export class RichEditorComponent extends StatefulControlComponent<{}, EditorValu } } -function buildContentTitle(content: ContentDto, language: LanguageDto) { +function buildContentTitle(content: ContentDto, language: AppLanguageDto) { const name = content.referenceFields .map(f => getContentValue(content, language, f, false)) diff --git a/frontend/src/app/shared/components/references/content-selector-item.component.ts b/frontend/src/app/shared/components/references/content-selector-item.component.ts index ca95c310d..da9cd32fc 100644 --- a/frontend/src/app/shared/components/references/content-selector-item.component.ts +++ b/frontend/src/app/shared/components/references/content-selector-item.component.ts @@ -11,7 +11,7 @@ import { booleanAttribute, ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { StopClickDirective } from '@app/framework'; -import { ContentDto, LanguageDto, META_FIELDS, SchemaDto } from '@app/shared/internal'; +import { AppLanguageDto, ContentDto, META_FIELDS, SchemaDto } from '@app/shared/internal'; import { ContentListCellDirective } from '../contents/content-list-cell.directive'; import { ContentListFieldComponent } from '../contents/content-list-field.component'; @@ -41,10 +41,10 @@ export class ContentSelectorItemComponent { public selectable?: boolean | null = true; @Input() - public language!: LanguageDto; + public language!: AppLanguageDto; @Input() - public languages!: ReadonlyArray; + public languages!: ReadonlyArray; @Input() public schema!: SchemaDto; 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 a56ce6edb..bbf8f7ca4 100644 --- a/frontend/src/app/shared/components/references/content-selector.component.ts +++ b/frontend/src/app/shared/components/references/content-selector.component.ts @@ -11,7 +11,7 @@ import { FormsModule } from '@angular/forms'; import { BehaviorSubject, of } from 'rxjs'; import { distinctUntilChanged, map, switchMap } from 'rxjs/operators'; import { LanguageSelectorComponent, ListViewComponent, ModalDialogComponent, PagerComponent, SyncWidthDirective, TooltipDirective, TranslatePipe } from '@app/framework'; -import { ApiUrlConfig, AppsState, ComponentContentsState, ContentDto, LanguageDto, META_FIELDS, Query, SchemaDto, SchemasService, SchemasState, Subscriptions } from '@app/shared/internal'; +import { ApiUrlConfig, AppLanguageDto, AppsState, ComponentContentsState, ContentDto, META_FIELDS, Query, SchemaDto, SchemasService, SchemasState, Subscriptions } from '@app/shared/internal'; import { ContentListCellDirective, ContentListWidthDirective } from '../contents/content-list-cell.directive'; import { ContentListHeaderComponent } from '../contents/content-list-header.component'; import { SearchFormComponent } from '../search/search-form.component'; @@ -64,10 +64,10 @@ export class ContentSelectorComponent implements OnInit { public query?: string; @Input({ required: true }) - public language!: LanguageDto; + public language!: AppLanguageDto; @Input({ required: true }) - public languages!: ReadonlyArray; + public languages!: ReadonlyArray; @Input({ transform: booleanAttribute }) public allowDuplicates?: boolean | null; diff --git a/frontend/src/app/shared/components/references/reference-input.component.ts b/frontend/src/app/shared/components/references/reference-input.component.ts index 7570d13a9..6b83005c0 100644 --- a/frontend/src/app/shared/components/references/reference-input.component.ts +++ b/frontend/src/app/shared/components/references/reference-input.component.ts @@ -8,7 +8,7 @@ import { booleanAttribute, ChangeDetectionStrategy, Component, forwardRef, Input } from '@angular/core'; import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ModalDirective, StopClickDirective } from '@app/framework'; -import { AppsState, ContentDto, ContentsService, DialogModel, getContentValue, LanguageDto, LocalizerService, StatefulControlComponent, TypedSimpleChanges, Types } from '@app/shared/internal'; +import { AppLanguageDto, AppsState, ContentDto, ContentsService, DialogModel, getContentValue, LocalizerService, StatefulControlComponent, TypedSimpleChanges, Types } from '@app/shared/internal'; import { ContentSelectorComponent } from './content-selector.component'; export const SQX_REFERENCE_INPUT_CONTROL_VALUE_ACCESSOR: any = { @@ -47,10 +47,10 @@ export class ReferenceInputComponent extends StatefulControlComponent; + public languages!: ReadonlyArray; @Input({ required: true }) public mode: 'Array' | 'Single' = 'Single'; diff --git a/frontend/src/app/shared/components/search/queries/filter-comparison.component.ts b/frontend/src/app/shared/components/search/queries/filter-comparison.component.ts index 7b0454576..372f4b3bd 100644 --- a/frontend/src/app/shared/components/search/queries/filter-comparison.component.ts +++ b/frontend/src/app/shared/components/search/queries/filter-comparison.component.ts @@ -9,7 +9,7 @@ import { AsyncPipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { DateTimeEditorComponent, DropdownComponent, HighlightPipe, TranslatePipe } from '@app/framework'; -import { ContributorsState, FilterableField, FilterComparison, FilterFieldUI, FilterNegation, getFilterUI, isNegation, LanguageDto, QueryModel } from '@app/shared/internal'; +import { AppLanguageDto, ContributorsState, FilterableField, FilterComparison, FilterFieldUI, FilterNegation, getFilterUI, isNegation, QueryModel } from '@app/shared/internal'; import { UserDtoPicture } from '../../pipes'; import { ReferenceInputComponent } from '../../references/reference-input.component'; import { QueryPathComponent } from './query-path.component'; @@ -42,10 +42,10 @@ export class FilterComparisonComponent { public remove = new EventEmitter(); @Input({ required: true }) - public language!: LanguageDto; + public language!: AppLanguageDto; @Input({ required: true }) - public languages!: ReadonlyArray; + public languages!: ReadonlyArray; @Input({ required: true }) public model!: QueryModel; diff --git a/frontend/src/app/shared/components/search/queries/filter-logical.component.ts b/frontend/src/app/shared/components/search/queries/filter-logical.component.ts index fa6947d36..af080f32f 100644 --- a/frontend/src/app/shared/components/search/queries/filter-logical.component.ts +++ b/frontend/src/app/shared/components/search/queries/filter-logical.component.ts @@ -8,7 +8,7 @@ import { booleanAttribute, ChangeDetectionStrategy, Component, EventEmitter, Input, numberAttribute, Output } from '@angular/core'; import { TranslatePipe } from '@app/framework'; -import { FilterLogical, FilterNode, isLogicalAnd, isLogicalOr, LanguageDto, QueryModel } from '@app/shared/internal'; +import { AppLanguageDto, FilterLogical, FilterNode, isLogicalAnd, isLogicalOr, QueryModel } from '@app/shared/internal'; import { FilterNodeComponent } from './filter-node.component'; @Component({ @@ -30,10 +30,10 @@ export class FilterLogicalComponent { public remove = new EventEmitter(); @Input({ required: true }) - public language!: LanguageDto; + public language!: AppLanguageDto; @Input({ required: true }) - public languages!: ReadonlyArray; + public languages!: ReadonlyArray; @Input({ transform: numberAttribute }) public level = 0; diff --git a/frontend/src/app/shared/components/search/queries/filter-node.component.ts b/frontend/src/app/shared/components/search/queries/filter-node.component.ts index ff3431b21..7a3994673 100644 --- a/frontend/src/app/shared/components/search/queries/filter-node.component.ts +++ b/frontend/src/app/shared/components/search/queries/filter-node.component.ts @@ -7,7 +7,7 @@ import { ChangeDetectionStrategy, Component, EventEmitter, forwardRef, Input, numberAttribute, Output } from '@angular/core'; -import { FilterComparison, FilterLogical, FilterNegation, FilterNode, isLogical, LanguageDto, QueryModel } from '@app/shared/internal'; +import { AppLanguageDto, FilterComparison, FilterLogical, FilterNegation, FilterNode, isLogical, QueryModel } from '@app/shared/internal'; import { FilterComparisonComponent } from './filter-comparison.component'; import { FilterLogicalComponent } from './filter-logical.component'; @@ -30,10 +30,10 @@ export class FilterNodeComponent { public remove = new EventEmitter(); @Input({ required: true }) - public language!: LanguageDto; + public language!: AppLanguageDto; @Input({ required: true }) - public languages!: ReadonlyArray; + public languages!: ReadonlyArray; @Input({ transform: numberAttribute }) public level = 0; diff --git a/frontend/src/app/shared/components/search/queries/query.component.ts b/frontend/src/app/shared/components/search/queries/query.component.ts index 7fb75ff91..261d7bb33 100644 --- a/frontend/src/app/shared/components/search/queries/query.component.ts +++ b/frontend/src/app/shared/components/search/queries/query.component.ts @@ -8,7 +8,7 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { TranslatePipe } from '@app/framework'; -import { FilterLogical, LanguageDto, Query, QueryModel, QuerySorting } from '@app/shared/internal'; +import { AppLanguageDto, FilterLogical, Query, QueryModel, QuerySorting } from '@app/shared/internal'; import { FilterLogicalComponent } from './filter-logical.component'; import { SortingComponent } from './sorting.component'; @@ -29,10 +29,10 @@ export class QueryComponent { public queryChange = new EventEmitter(); @Input({ required: true }) - public language!: LanguageDto; + public language!: AppLanguageDto; @Input({ required: true }) - public languages!: ReadonlyArray; + public languages!: ReadonlyArray; @Input({ required: true }) public model!: QueryModel; diff --git a/frontend/src/app/shared/components/search/search-form.component.ts b/frontend/src/app/shared/components/search/search-form.component.ts index 814aaf1d3..ac8055112 100644 --- a/frontend/src/app/shared/components/search/search-form.component.ts +++ b/frontend/src/app/shared/components/search/search-form.component.ts @@ -10,7 +10,7 @@ import { booleanAttribute, ChangeDetectionStrategy, Component, EventEmitter, Inp import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { Observable } from 'rxjs'; import { ControlErrorsComponent, FocusOnInitDirective, MarkdownDirective, ModalDialogComponent, ModalDirective, ShortcutComponent, ShortcutDirective, TooltipDirective, TourStepDirective, TranslatePipe } from '@app/framework'; -import { DialogModel, equalsQuery, hasFilter, LanguageDto, Queries, Query, QueryModel, SaveQueryForm, TypedSimpleChanges } from '@app/shared/internal'; +import { AppLanguageDto, DialogModel, equalsQuery, hasFilter, Queries, Query, QueryModel, SaveQueryForm, TypedSimpleChanges } from '@app/shared/internal'; import { TourHintDirective } from '../tour-hint.directive'; import { QueryComponent } from './queries/query.component'; import { SavedQueriesComponent } from './shared-queries.component'; @@ -50,10 +50,10 @@ export class SearchFormComponent { public placeholder = ''; @Input() - public language!: LanguageDto; + public language!: AppLanguageDto; @Input() - public languages: ReadonlyArray = []; + public languages: ReadonlyArray = []; @Input() public queryModel?: QueryModel | null; @@ -116,19 +116,20 @@ export class SearchFormComponent { public saveQueryComplete() { const value = this.saveQueryForm.submit(); + if (!value) { + return; + } - if (value) { - if (this.queries && this.query) { - if (value.user) { - this.queries.addUser(value.name, this.query); - } else { - this.queries.addShared(value.name, this.query); - } + if (this.queries && this.query) { + if (value.user) { + this.queries.addUser(value.name, this.query); + } else { + this.queries.addShared(value.name, this.query); } - - this.saveQueryForm.submitCompleted(); - this.saveQueryDialog.hide(); } + + this.saveQueryForm.submitCompleted(); + this.saveQueryDialog.hide(); } public changeQueryFullText(fullText: string) { diff --git a/frontend/src/app/shared/components/table-header.component.ts b/frontend/src/app/shared/components/table-header.component.ts index 0954bdec4..aaf85f1a3 100644 --- a/frontend/src/app/shared/components/table-header.component.ts +++ b/frontend/src/app/shared/components/table-header.component.ts @@ -8,7 +8,7 @@ import { booleanAttribute, ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { TranslatePipe } from '@app/framework'; -import { LanguageDto, Query, SortMode, Types } from '@app/shared/internal'; +import { AppLanguageDto, Query, SortMode, Types } from '@app/shared/internal'; @Component({ standalone: true, @@ -31,7 +31,7 @@ export class TableHeaderComponent { public text = ''; @Input() - public language!: LanguageDto; + public language!: AppLanguageDto; @Input() public sortPath?: string | undefined | null; diff --git a/frontend/src/app/shared/components/team-form.component.ts b/frontend/src/app/shared/components/team-form.component.ts index e3de79eca..513589c07 100644 --- a/frontend/src/app/shared/components/team-form.component.ts +++ b/frontend/src/app/shared/components/team-form.component.ts @@ -50,17 +50,18 @@ export class TeamFormComponent { public createTeam() { const value = this.createForm.submit(); - - if (value) { - this.teamsStore.create(value) - .subscribe({ - next: () => { - this.emitClose(); - }, - error: error => { - this.createForm.submitFailed(error); - }, - }); + if (!value) { + return; } + + this.teamsStore.create(value) + .subscribe({ + next: () => { + this.emitClose(); + }, + error: error => { + this.createForm.submitFailed(error); + }, + }); } } diff --git a/frontend/src/app/shared/guards/rule-must-exist.guard.spec.ts b/frontend/src/app/shared/guards/rule-must-exist.guard.spec.ts index 44f43acea..8b46cc363 100644 --- a/frontend/src/app/shared/guards/rule-must-exist.guard.spec.ts +++ b/frontend/src/app/shared/guards/rule-must-exist.guard.spec.ts @@ -9,7 +9,7 @@ import { TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; import { firstValueFrom, of } from 'rxjs'; import { IMock, It, Mock, Times } from 'typemoq'; -import { RuleDto, RulesState } from '@app/shared/internal'; +import { DynamicRuleDto, RulesState } from '@app/shared/internal'; import { ruleMustExistGuard } from './rule-must-exist.guard'; describe('RuleMustExistGuard', () => { @@ -36,7 +36,7 @@ describe('RuleMustExistGuard', () => { bit('should load rule and return true if found', async () => { rulesState.setup(x => x.select('123')) - .returns(() => of({})); + .returns(() => of({})); const route: any = { params: { diff --git a/frontend/src/app/shared/internal.ts b/frontend/src/app/shared/internal.ts index 0c1834807..027563ca3 100644 --- a/frontend/src/app/shared/internal.ts +++ b/frontend/src/app/shared/internal.ts @@ -8,6 +8,7 @@ export * from '@app/framework'; export * from './components/cards/shared'; export * from './interceptors/auth.interceptor'; +export * from './model'; export * from './services/app-languages.service'; export * from './services/apps.service'; export * from './services/assets.service'; @@ -28,9 +29,7 @@ export * from './services/query'; export * from './services/roles.service'; export * from './services/rules.service'; export * from './services/schemas.service'; -export * from './services/schemas.types'; export * from './services/search.service'; -export * from './services/shared'; export * from './services/stock-photo.service'; export * from './services/teams.service'; export * from './services/templates.service'; diff --git a/frontend/src/app/shared/model/custom.ts b/frontend/src/app/shared/model/custom.ts new file mode 100644 index 000000000..501321755 --- /dev/null +++ b/frontend/src/app/shared/model/custom.ts @@ -0,0 +1,962 @@ +/* eslint-disable sort-imports */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { hasAnyLink, DateTime, StringHelper, Types, ApiUrlConfig, ErrorDto } from '@app/framework'; +import * as generated from './generated'; +import { FieldPropertiesVisitor, META_FIELDS, tableField, tableFields } from './schemas'; + +export class AppDto extends generated.AppDto { + get displayName() { + return this.compute('displayName', () => StringHelper.firstNonEmpty(this.label, this.name)); + } + + get canCreateSchema() { + return this.compute('canCreateSchema', () => hasAnyLink(this._links, 'schemas/create')); + } + + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'delete')); + } + + get canLeave() { + return this.compute('canLeave', () => hasAnyLink(this._links, 'leave')); + } + + get canReadAssets() { + return this.compute('canReadAssets', () => hasAnyLink(this._links, 'assets')); + } + + get canReadAssetsScripts() { + return this.compute('canReadAssetsScripts', () => hasAnyLink(this._links, 'assets/scripts')); + } + + get canReadClients() { + return this.compute('canReadClients', () => hasAnyLink(this._links, 'clients')); + } + + get canReadContributors() { + return this.compute('canReadContributors', () => hasAnyLink(this._links, 'contributors')); + } + + get canReadJobs() { + return this.compute('canReadJobs', () => hasAnyLink(this._links, 'jobs')); + } + + get canReadLanguages() { + return this.compute('canReadLanguages', () => hasAnyLink(this._links, 'languages')); + } + + get canReadPatterns() { + return this.compute('canReadPatterns', () => hasAnyLink(this._links, 'patterns')); + } + + get canReadPlans() { + return this.compute('canReadPlans', () => hasAnyLink(this._links, 'plans')); + } + + get canReadRoles() { + return this.compute('canReadRoles', () => hasAnyLink(this._links, 'roles')); + } + + get canReadRules() { + return this.compute('canReadRules', () => hasAnyLink(this._links, 'rules')); + } + + get canReadSchemas() { + return this.compute('canReadSchemas', () => hasAnyLink(this._links, 'schemas')); + } + + get canReadWorkflows() { + return this.compute('canReadWorkflows', () => hasAnyLink(this._links, 'workflows')); + } + + get canUpdateGeneral() { + return this.compute('canUpdateGeneral', () => hasAnyLink(this._links, 'update')); + } + + get canUpdateImage() { + return this.compute('canUpdateImage', () => hasAnyLink(this._links, 'image/upload')); + } + + get canUpdateTeam() { + return this.compute('canUpdateTeam', () => hasAnyLink(this._links, 'transfer')); + } + + get canUploadAssets() { + return this.compute('canUploadAssets', () => hasAnyLink(this._links, 'assets/create')); + } + + get image() { + return this.compute('image', () => this._links['image']?.href); + } +} + +export class AppLanguageDto extends generated.AppLanguageDto { + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'delete')); + } + + get canUpdate() { + return this.compute('canUpdate', () => hasAnyLink(this._links, 'update')); + } +} + +export class AppLanguagesDto extends generated.AppLanguagesDto { + get canCreate() { + return this.compute('canCreate', () => hasAnyLink(this._links, 'create')); + } +} + +export class AppSettingsDto extends generated.AppSettingsDto { + get canUpdate() { + return this.compute('canUpdate', () => hasAnyLink(this._links, 'update')); + } +} + +export class AssetDto extends generated.AssetDto { + public get isDuplicate() { + return this.compute('isDuplicate', () => this._meta && this._meta['isDuplicate'] === 'true'); + } + + public get contentUrl() { + return this.compute('contentUrl', () => this._links['content']?.href); + } + + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'update')); + } + + get canMove() { + return this.compute('canMove', () => hasAnyLink(this._links, 'move')); + } + + get canUpdate() { + return this.compute('canUpdate', () => hasAnyLink(this._links, 'update')); + } + + get canUpload() { + return this.compute('canUpload', () => hasAnyLink(this._links, 'upload')); + } + + get canPreview() { + return this.compute('canPreview', () => { + const SVG_PREVIEW_LIMIT = 10 * 1024; + const MIME_TIFF = 'image/tiff'; + const MIME_SVG = 'image/svg+xml'; + + const canPreview = + (this.mimeType !== MIME_TIFF && this.type === 'Image') || + (this.mimeType === MIME_SVG && this.fileSize < SVG_PREVIEW_LIMIT); + + return canPreview; + }); + } + + public get fileNameWithoutExtension() { + return this.compute('fileNameWithoutExtension', () => { + const index = this.fileName.lastIndexOf('.'); + + if (index > 0) { + return this.fileName.substring(0, index); + } else { + return this.fileName; + } + + }); + } + + public fullUrl(apiUrl: ApiUrlConfig) { + return apiUrl.buildUrl(this.contentUrl); + } +} + +export class AssetsDto extends generated.AssetsDto { + get canCreate() { + return this.compute('canCreate', () => hasAnyLink(this._links, 'create')); + } + + get canRenameTag() { + return this.compute('canRenameTag', () => hasAnyLink(this._links, 'tags/rename')); + } +} + +export class AssetFolderDto extends generated.AssetFolderDto { + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'update')); + } + + get canMove() { + return this.compute('canMove', () => hasAnyLink(this._links, 'move')); + } + + get canUpdate() { + return this.compute('canUpdate', () => hasAnyLink(this._links, 'update')); + } +} + +export class AssetFoldersDto extends generated.AssetsDto { + get canCreate() { + return this.compute('canCreate', () => hasAnyLink(this._links, 'create')); + } +} + +export class AssetScriptsDto extends generated.AssetScriptsDto { + get canUpdate() { + return this.compute('canUpdate', () => hasAnyLink(this._links, 'update')); + } +} + +export class AuthSchemeResponseDto extends generated.AuthSchemeResponseDto { + get canUpdate() { + return this.compute('canUpdate', () => hasAnyLink(this._links, 'update')); + } +} + +export class ClientDto extends generated.ClientDto { + get canRevoke() { + return this.compute('canRevoke', () => hasAnyLink(this._links, 'delete')); + } + + get canUpdate() { + return this.compute('canUpdate', () => hasAnyLink(this._links, 'update')); + } +} + +export class ClientsDto extends generated.ClientsDto { + get canCreate() { + return this.compute('canCreate', () => hasAnyLink(this._links, 'create')); + } +} + +export class ContentDto extends generated.ContentDto { + get canPublish() { + return this.compute('canPublish', () => this.statusUpdates.find(x => x.status === 'Published')); + } + + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'update')); + } + + get canDraftCreate() { + return this.compute('canDraftCreate', () => hasAnyLink(this._links, 'draft/create')); + } + + get canDraftDelete() { + return this.compute('canDraftDelete', () => hasAnyLink(this._links, 'draft/delete')); + } + + get canCancelStatus() { + return this.compute('canCancelStatus', () => hasAnyLink(this._links, 'cancel')); + } + + get canUpdate() { + return this.compute('canUpdate', () => hasAnyLink(this._links, 'update')); + } + + get statusUpdates() { + return this.compute('statusUpdates', () => { + const updates: { status: string; color: string }[] = []; + for (const [key, link] of Object.entries(this._links)) { + if (key.startsWith('status/')) { + updates.push({ status: key.substring(7), color: link.metadata! }); + } + } + + return updates; + }); + } +} + +export class ContentsDto extends generated.ContentsDto { + get canCreate() { + return this.compute('canCreate', () => hasAnyLink(this._links, 'create')); + } + + get canCreateAndPublish() { + return this.compute('canCreate', () => hasAnyLink(this._links, 'create/publish')); + } +} + +export class ContributorDto extends generated.ContributorDto { + public get token() { + return `subject:${this.contributorId}`; + } + + get canRevoke() { + return this.compute('canRevoke ', () => hasAnyLink(this._links, 'update')); + } + + get canUpdate() { + return this.compute('canUpdate ', () => hasAnyLink(this._links, 'update')); + } +} + +export class ContributorsDto extends generated.ContributorsDto { + get canCreate() { + return this.compute('canCreate', () => hasAnyLink(this._links, 'create')); + } + + get isInvited() { + return this.compute('isInvited', () => this._meta?.['isInvited'] === '1'); + } +} + +export class EventConsumerDto extends generated.UserDto { + get canReset() { + return this.compute('reset', () => hasAnyLink(this._links, 'reset')); + } + + get canStart() { + return this.compute('canStart', () => hasAnyLink(this._links, 'start')); + } + + get canStop() { + return this.compute('canStop', () => hasAnyLink(this._links, 'stop')); + } +} + +export class FieldDto extends generated.FieldDto { + public get rawProperties(): any { + return this.properties; + } + + public get isInlineEditable(): boolean { + return this.compute('isInlineEditable', () => !this.isDisabled && this.rawProperties.inlineEditable === true); + } + + public get isInvariant(): boolean { + return this.compute('isInvariant', () => this.partitioning === 'invariant'); + } + + public get isLocalizable(): boolean { + return this.compute('isLocalizable', () => this.partitioning === 'language'); + } + + public get displayName() { + return this.compute('displayName', () => StringHelper.firstNonEmpty(this.properties.label, this.name)); + } + + public get displayPlaceholder() { + return this.compute('displayPlaceholder', () => this.properties.placeholder || ''); + } + + get canAddField() { + return this.compute('canAddField', () => hasAnyLink(this._links, 'fields/add')); + } + + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'delete')); + } + + get canDisable() { + return this.compute('canDisable', () => hasAnyLink(this._links, 'disable')); + } + + get canEnable() { + return this.compute('canEnable', () => hasAnyLink(this._links, 'enable')); + } + + get canOrderFields() { + return this.compute('canOrderFields', () => hasAnyLink(this._links, 'fields/order')); + } + + get canHide() { + return this.compute('canHide', () => hasAnyLink(this._links, 'hide')); + } + + get canLock() { + return this.compute('canLock', () => hasAnyLink(this._links, 'lock')); + } + + get canShow() { + return this.compute('canShow', () => hasAnyLink(this._links, 'show')); + } + + get canUpdate() { + return this.compute('canUpdate', () => hasAnyLink(this._links, 'update')); + } +} + +export class NestedFieldDto extends generated.NestedFieldDto { + public get rawProperties(): any { + return this.properties; + } + + public get displayName() { + return this.compute('displayName', () => StringHelper.firstNonEmpty(this.properties.label, this.name)); + } + + public get displayPlaceholder() { + return this.compute('displayPlaceholder', () => this.properties.placeholder || ''); + } + + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'delete')); + } + + get canDisable() { + return this.compute('canDisable', () => hasAnyLink(this._links, 'disable')); + } + + get canEnable() { + return this.compute('canEnable', () => hasAnyLink(this._links, 'enable')); + } + get canHide() { + return this.compute('canHide', () => hasAnyLink(this._links, 'hide')); + } + + get canLock() { + return this.compute('canLock', () => hasAnyLink(this._links, 'lock')); + } + + get canShow() { + return this.compute('canShow', () => hasAnyLink(this._links, 'show')); + } + + get canUpdate() { + return this.compute('canUpdate', () => hasAnyLink(this._links, 'update')); + } +} + +export class IndexDto extends generated.IndexDto { + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'delete')); + } +} + +export class IndexesDto extends generated.IndexesDto { + get canCreate() { + return this.compute('canCreate', () => hasAnyLink(this._links, 'create')); + } +} + +export class JobDto extends generated.JobDto { + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'delete')); + } + + get canDownloadUrl() { + return this.compute('canDownloadUrl', () => hasAnyLink(this._links, 'download')); + } + + get downloadUrl() { + return this.compute('downloadUrl', () => this._links['download']?.href); + } + + get isfailed() { + return this.status === 'Failed'; + } +} + +export class JobsDto extends generated.JobsDto { + get canCreateBackup() { + return this.compute('canCreateBackup', () => hasAnyLink(this._links, 'create/backups')); + } +} + +export class RoleDto extends generated.RoleDto { + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'update')); + } + + get canUpdate() { + return this.compute('canUpdate', () => hasAnyLink(this._links, 'update')); + } +} + +export class RolesDto extends generated.RolesDto { + get canCreate() { + return this.compute('canCreate', () => hasAnyLink(this._links, 'create')); + } +} + +export class SchemaDto extends generated.SchemaDto { + get canAddField() { + return this.compute('canAddField', () => hasAnyLink(this._links, 'fields/add')); + } + + get canContentsCreate() { + return this.compute('canContentsCreate', () => hasAnyLink(this._links, 'contents/create')); + } + + get canContentsCreateAndPublish() { + return this.compute('canContentsCreateAndPublish', () => hasAnyLink(this._links, 'contents/create/publish')); + } + + get canContentsRead() { + return this.compute('canContentsCreateAndPublish', () => hasAnyLink(this._links, 'contents')); + } + + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'delete')); + } + + get canOrderFields() { + return this.compute('canOrderFields', () => hasAnyLink(this._links, 'fields/order')); + } + + get canPublish() { + return this.compute('canPublish', () => hasAnyLink(this._links, 'publish')); + } + + get canReadContents() { + return this.compute('canReadContents', () => hasAnyLink(this._links, 'contents')); + } + + get canSynchronize() { + return this.compute('canReadContents', () => hasAnyLink(this._links, 'update/sync')); + } + + get canUnpublish() { + return this.compute('canReadContents', () => hasAnyLink(this._links, 'unpublish')); + } + + get canUpdate() { + return this.compute('canReadContents', () => hasAnyLink(this._links, 'update')); + } + + get canUpdateCategory() { + return this.compute('canUpdateCategory', () => hasAnyLink(this._links, 'update/category')); + } + + get canUpdateRules() { + return this.compute('canUpdateCategory', () => hasAnyLink(this._links, 'update/rules')); + } + + get canUpdateScripts() { + return this.compute('canUpdateScripts', () => hasAnyLink(this._links, 'update/scripts')); + } + + get canUpdateUIFields() { + return this.compute('canUpdateUIFields', () => hasAnyLink(this._links, 'fields/ui')); + } + + get canUpdateUrls() { + return this.compute('canUpdateUrls', () => hasAnyLink(this._links, 'update/urls')); + } + + get displayName() { + return this.compute('displayName', () => StringHelper.firstNonEmpty(this.properties.label, this.name)); + } + + get contentFields() { + return this.compute('displayName', () => this.fields.filter(x => x.properties.isContentField).map(tableField)); + } + + get defaultListFields() { + return this.compute('defaultListFields', () => { + const listFields = tableFields(this.fieldsInLists, this.contentFields); + + if (listFields.length === 0) { + listFields.push(META_FIELDS.lastModifiedByAvatar); + + if (this.fields.length > 0) { + listFields.push(tableField(this.fields[0])); + } else { + listFields.push(META_FIELDS.empty); + } + + listFields.push(META_FIELDS.statusColor); + listFields.push(META_FIELDS.lastModified); + } + + return listFields; + }); + } + + get defaultReferenceFields() { + return this.compute('defaultReferenceFields', () => { + const referenceFields = tableFields(this.fieldsInReferences, this.contentFields); + + if (referenceFields.length === 0) { + if (this.fields.length > 0) { + referenceFields.push(tableField(this.fields[0])); + } else { + referenceFields.push(META_FIELDS.empty); + } + } + + return referenceFields; + }); + } + + public export(): any { + const fieldKeys = [ + 'fieldId', + 'parentId', + 'parentFieldId', + '_links', + ]; + + const cleanup = (source: any, ...exclude: string[]): any => { + const clone = {} as Record; + + for (const [key, value] of Object.entries(source)) { + if (!exclude.includes(key) && key.indexOf('can') !== 0 && !Types.isUndefined(value) && !Types.isNull(value)) { + clone[key] = value; + } + } + + return clone; + }; + + const result: any = { + previewUrls: this.previewUrls, + properties: cleanup(this.properties), + category: this.category, + scripts: this.scripts, + isPublished: this.isPublished, + fieldRules: this.fieldRules, + fieldsInLists: this.fieldsInLists, + fieldsInReferences: this.fieldsInReferences, + fields: this.fields.map(field => { + const copy = cleanup(field, ...fieldKeys); + + copy.properties = cleanup(field.properties); + + if (Types.isArray(copy.nested)) { + if (copy.nested.length === 0) { + delete copy['nested']; + } else if (field.nested) { + copy.nested = field.nested.map(nestedField => { + const nestedCopy = cleanup(nestedField, ...fieldKeys); + + nestedCopy.properties = cleanup(nestedField.properties); + + return nestedCopy; + }); + } + } + + return copy; + }), + type: this.type, + }; + + return result; + } +} + +export class DynamicRuleDto extends generated.DynamicRuleDto { + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'delete')); + } + + get canDisable() { + return this.compute('canDisable', () => hasAnyLink(this._links, 'disable')); + } + + get canEnable() { + return this.compute('canEnable', () => hasAnyLink(this._links, 'enable')); + } + + get canReadLogs() { + return this.compute('canReadLogs', () => hasAnyLink(this._links, 'logs')); + } + + get canRun() { + return this.compute('canRun', () => hasAnyLink(this._links, 'run')); + } + + get canRunFromSnapshots() { + return this.compute('canRunFromSnapshots', () => hasAnyLink(this._links, 'run/snapshots')); + } + + get canTrigger() { + return this.compute('canTrigger', () => hasAnyLink(this._links, 'trigger')); + } + + get canUpdate() { + return this.compute('canUpdate', () => hasAnyLink(this._links, 'update')); + } +} + +export class DynamicRulesDto extends generated.DynamicRulesDto { + get canCreate() { + return this.compute('canCreate', () => hasAnyLink(this._links, 'create')); + } + + get canReadEvents() { + return this.compute('canReadEvents', () => hasAnyLink(this._links, 'events')); + } + + get canCancelRun() { + return this.compute('canCancelRun', () => hasAnyLink(this._links, 'run/cancel')); + } +} + +export class RuleEventDto extends generated.RuleEventDto { + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'cancel')); + } + + get canUpdate() { + return this.compute('canUpdate', () => hasAnyLink(this._links, 'update')); + } +} + +export class RuleEventsDto extends generated.RuleEventsDto { + get canCancelAll() { + return this.compute('canCancelAll ', () => hasAnyLink(this._links, 'cancel')); + } +} + +export class SearchResultDto extends generated.SearchResultDto { + get url() { + return this.compute('url', () => this._links['url'].href); + } +} + +export class TeamDto extends generated.TeamDto { + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'delete')); + } + + get canReadAuth() { + return this.compute('canReadAuth', () => hasAnyLink(this._links, 'auth')); + } + + get canReadContributors() { + return this.compute('canReadContributors', () => hasAnyLink(this._links, 'contributors')); + } + + get canReadPlans() { + return this.compute('canReadPlans', () => hasAnyLink(this._links, 'plans')); + } + + get canUpdateGeneral() { + return this.compute('canUpdateGeneral', () => hasAnyLink(this._links, 'update')); + } +} + +export class SchemasDto extends generated.SchemasDto { + get canCreate() { + return this.compute('canCreate', () => hasAnyLink(this._links, 'create')); + } +} + +export class ServerErrorDto extends generated.ServerErrorDto { + toError(): ErrorDto { + return new ErrorDto( + this.statusCode, + this.message, + this.errorCode, + this.details); + } +} + +export class UserDto extends generated.UserDto { + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'delete')); + } + + get canLock() { + return this.compute('canLock', () => hasAnyLink(this._links, 'lock')); + } + + get canUnlock() { + return this.compute('canUnlock', () => hasAnyLink(this._links, 'unlock')); + } + + get canUpdate() { + return this.compute('canUpdate', () => hasAnyLink(this._links, 'update')); + } +} + +export class UsersDto extends generated.UsersDto { + get canCreate() { + return this.compute('canCreate', () => hasAnyLink(this._links, 'create')); + } +} + +export class WorkflowDto extends generated.WorkflowDto { + get displayName() { + return this.compute('displayName', () => StringHelper.firstNonEmpty(this.name, 'i18n:workflows.notNamed')); + } + + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'delete')); + } + + get canUpdate() { + return this.compute('canUpdate', () => hasAnyLink(this._links, 'update')); + } +} + +export class WorkflowsDto extends generated.WorkflowsDto { + get canCreate() { + return this.compute('canCreate', () => hasAnyLink(this._links, 'create')); + } +} + +// +// FIELD TYPES +// +export class FieldPropertiesDto extends generated.FieldPropertiesDto { + public get isComplexUI() { + return true; + } + + public get isSortable() { + return true; + } + + public get isContentField() { + return true; + } + + public accept(_visitor: FieldPropertiesVisitor): T { + throw new Error('NOT IMPLEMENTED'); + } +} + +export class ArrayFieldPropertiesDto extends generated.ArrayFieldPropertiesDto { + public accept(visitor: FieldPropertiesVisitor): T { + return visitor.visitArray(this); + } +} + +export class AssetsFieldPropertiesDto extends generated.AssetsFieldPropertiesDto { + public get isSortable() { + return false; + } + + public accept(visitor: FieldPropertiesVisitor): T { + return visitor.visitAssets(this); + } +} + +export class BooleanFieldPropertiesDto extends generated.BooleanFieldPropertiesDto { + public get isComplexUI() { + return false; + } + + public accept(visitor: FieldPropertiesVisitor): T { + return visitor.visitBoolean(this); + } +} + +export class ComponentFieldPropertiesDto extends generated.ComponentFieldPropertiesDto { + public get isComplexUI() { + return true; + } + + public accept(visitor: FieldPropertiesVisitor): T { + return visitor.visitComponent(this); + } +} + +export class ComponentsFieldPropertiesDto extends generated.ComponentsFieldPropertiesDto { + public get isComplexUI() { + return true; + } + + public accept(visitor: FieldPropertiesVisitor): T { + return visitor.visitComponents(this); + } +} + +export class DateTimeFieldPropertiesDto extends generated.DateTimeFieldPropertiesDto { + public get isComplexUI() { + return false; + } + + public accept(visitor: FieldPropertiesVisitor): T { + return visitor.visitDateTime(this); + } +} + +export class GeolocationFieldPropertiesDto extends generated.GeolocationFieldPropertiesDto { + public get isSortable() { + return false; + } + + public accept(visitor: FieldPropertiesVisitor): T { + return visitor.visitGeolocation(this); + } +} + +export class JsonFieldPropertiesDto extends generated.JsonFieldPropertiesDto { + public get isSortable() { + return false; + } + + public accept(visitor: FieldPropertiesVisitor): T { + return visitor.visitJson(this); + } +} + +export class NumberFieldPropertiesDto extends generated.NumberFieldPropertiesDto { + public get isComplexUI() { + return false; + } + + public accept(visitor: FieldPropertiesVisitor): T { + return visitor.visitNumber(this); + } +} + +export class ReferencesFieldPropertiesDto extends generated.ReferencesFieldPropertiesDto { + public get singleId() { + return this.schemaIds?.[0] || null; + } + + public get isSortable() { + return false; + } + + public accept(visitor: FieldPropertiesVisitor): T { + return visitor.visitReferences(this); + } +} + +export class RichTextFieldPropertiesDto extends generated.RichTextFieldPropertiesDto { + public get isSortable() { + return false; + } + + public accept(visitor: FieldPropertiesVisitor): T { + return visitor.visitRichText(this); + } +} + +export class StringFieldPropertiesDto extends generated.StringFieldPropertiesDto { + public get isComplexUI() { + return this.editor !== 'Input' && this.editor !== 'Color' && this.editor !== 'Radio' && this.editor !== 'Slug' && this.editor !== 'TextArea'; + } + + public accept(visitor: FieldPropertiesVisitor): T { + return visitor.visitString(this); + } +} + +export class TagsFieldPropertiesDto extends generated.TagsFieldPropertiesDto { + public get isComplexUI() { + return false; + } + + public get isSortable() { + return false; + } + + public accept(visitor: FieldPropertiesVisitor): T { + return visitor.visitTags(this); + } +} + +export class UIFieldPropertiesDto extends generated.UIFieldPropertiesDto { + public get isComplexUI() { + return false; + } + + public get isSortable() { + return false; + } + + public get isContentField() { + return false; + } + + public accept(visitor: FieldPropertiesVisitor): T { + return visitor.visitUI(this); + } +} diff --git a/frontend/src/app/shared/model/generated.ts b/frontend/src/app/shared/model/generated.ts new file mode 100644 index 000000000..3eab7c043 --- /dev/null +++ b/frontend/src/app/shared/model/generated.ts @@ -0,0 +1,14839 @@ +//---------------------- +// +// Generated using the NSwag toolchain v14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org) +// +//---------------------- + +/* tslint:disable */ +/* eslint-disable */ +// ReSharper disable InconsistentNaming + +import { hasAnyLink, DateTime, StringHelper, Types, ApiUrlConfig, ErrorDto } from '@app/framework'; +import { FieldPropertiesVisitor, META_FIELDS, tableField, tableFields } from './schemas'; + +export class ServerErrorDto implements IServerErrorDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** Error message. */ + readonly message!: string; + /** The error code. */ + readonly errorCode?: string | undefined; + /** The optional trace id. */ + readonly traceId?: string | undefined; + /** Link to the error details. */ + readonly type?: string | undefined; + /** Detailed error messages. */ + readonly details?: string[] | undefined; + /** Status code of the http response. */ + readonly statusCode!: number; + + toError(): ErrorDto { + return new ErrorDto( + this.statusCode, + this.message, + this.errorCode, + this.details); + } + + constructor(data?: IServerErrorDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).message = _data["message"]; + (this).errorCode = _data["errorCode"]; + (this).traceId = _data["traceId"]; + (this).type = _data["type"]; + if (Array.isArray(_data["details"])) { + (this).details = [] as any; + for (let item of _data["details"]) + (this).details!.push(item); + } + (this).statusCode = _data["statusCode"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): ServerErrorDto { + const result = new ServerErrorDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["message"] = this.message; + data["errorCode"] = this.errorCode; + data["traceId"] = this.traceId; + data["type"] = this.type; + if (Array.isArray(this.details)) { + data["details"] = []; + for (let item of this.details) + data["details"].push(item); + } + data["statusCode"] = this.statusCode; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IServerErrorDto { + /** Error message. */ + readonly message: string; + /** The error code. */ + readonly errorCode?: string | undefined; + /** The optional trace id. */ + readonly traceId?: string | undefined; + /** Link to the error details. */ + readonly type?: string | undefined; + /** Detailed error messages. */ + readonly details?: string[] | undefined; + /** Status code of the http response. */ + readonly statusCode: number; +} + +export class UserPropertyDto implements IUserPropertyDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + readonly name!: string; + readonly value!: string; + + constructor(data?: IUserPropertyDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).name = _data["name"]; + (this).value = _data["value"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): UserPropertyDto { + const result = new UserPropertyDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["name"] = this.name; + data["value"] = this.value; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IUserPropertyDto { + readonly name: string; + readonly value: string; +} + +export class UpdateSettingDto implements IUpdateSettingDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The value for the setting. */ + readonly value!: any; + + constructor(data?: IUpdateSettingDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).value = _data["value"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): UpdateSettingDto { + const result = new UpdateSettingDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["value"] = this.value; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IUpdateSettingDto { + /** The value for the setting. */ + readonly value: any; +} + +export abstract class ResourceDto implements IResourceDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The links. */ + readonly _links!: { [key: string]: ResourceLinkDto; }; + + constructor(data?: IResourceDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + if (_data["_links"]) { + (this)._links = {} as any; + for (let key in _data["_links"]) { + if (_data["_links"].hasOwnProperty(key)) + ((this)._links)![key] = _data["_links"][key] ? ResourceLinkDto.fromJSON(_data["_links"][key]) : new ResourceLinkDto(); + } + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): ResourceDto { + throw new Error("The abstract class 'ResourceDto' cannot be instantiated."); + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (this._links) { + data["_links"] = {}; + for (let key in this._links) { + if (this._links.hasOwnProperty(key)) + (data["_links"])[key] = this._links[key] ? this._links[key].toJSON() : undefined; + } + } + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IResourceDto { + /** The links. */ + readonly _links: { [key: string]: ResourceLinkDto; }; +} + +export class UsersDto extends ResourceDto implements IUsersDto { + /** The total number of users. */ + readonly total!: number; + /** The users. */ + readonly items!: UserDto[]; + + get canCreate() { + return this.compute('canCreate', () => hasAnyLink(this._links, 'create')); + } + + constructor(data?: IUsersDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).total = _data["total"]; + if (Array.isArray(_data["items"])) { + (this).items = [] as any; + for (let item of _data["items"]) + (this).items!.push(UserDto.fromJSON(item)); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): UsersDto { + const result = new UsersDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["total"] = this.total; + if (Array.isArray(this.items)) { + data["items"] = []; + for (let item of this.items) + data["items"].push(item.toJSON()); + } + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IUsersDto extends IResourceDto { + /** The total number of users. */ + readonly total: number; + /** The users. */ + readonly items: UserDto[]; +} + +export class UserDto extends ResourceDto implements IUserDto { + /** The ID of the user. */ + readonly id!: string; + /** The email of the user. Unique value. */ + readonly email!: string; + /** The display name (usually first name and last name) of the user. */ + readonly displayName!: string; + /** Determines if the user is locked. */ + readonly isLocked!: boolean; + /** Additional permissions for the user. */ + readonly permissions!: string[]; + + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'delete')); + } + + get canLock() { + return this.compute('canLock', () => hasAnyLink(this._links, 'lock')); + } + + get canUnlock() { + return this.compute('canUnlock', () => hasAnyLink(this._links, 'unlock')); + } + + get canUpdate() { + return this.compute('canUpdate', () => hasAnyLink(this._links, 'update')); + } + + constructor(data?: IUserDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).id = _data["id"]; + (this).email = _data["email"]; + (this).displayName = _data["displayName"]; + (this).isLocked = _data["isLocked"]; + if (Array.isArray(_data["permissions"])) { + (this).permissions = [] as any; + for (let item of _data["permissions"]) + (this).permissions!.push(item); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): UserDto { + const result = new UserDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["id"] = this.id; + data["email"] = this.email; + data["displayName"] = this.displayName; + data["isLocked"] = this.isLocked; + if (Array.isArray(this.permissions)) { + data["permissions"] = []; + for (let item of this.permissions) + data["permissions"].push(item); + } + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IUserDto extends IResourceDto { + /** The ID of the user. */ + readonly id: string; + /** The email of the user. Unique value. */ + readonly email: string; + /** The display name (usually first name and last name) of the user. */ + readonly displayName: string; + /** Determines if the user is locked. */ + readonly isLocked: boolean; + /** Additional permissions for the user. */ + readonly permissions: string[]; +} + +export class ResourceLinkDto implements IResourceLinkDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The link url. */ + readonly href!: string; + /** The link method. */ + readonly method!: string; + /** Additional data about the link. */ + readonly metadata?: string | undefined; + + constructor(data?: IResourceLinkDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).href = _data["href"]; + (this).method = _data["method"]; + (this).metadata = _data["metadata"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): ResourceLinkDto { + const result = new ResourceLinkDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["href"] = this.href; + data["method"] = this.method; + data["metadata"] = this.metadata; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IResourceLinkDto { + /** The link url. */ + readonly href: string; + /** The link method. */ + readonly method: string; + /** Additional data about the link. */ + readonly metadata?: string | undefined; +} + +export class CreateUserDto implements ICreateUserDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The email of the user. Unique value. */ + readonly email!: string; + /** The display name (usually first name and last name) of the user. */ + readonly displayName!: string; + /** The password of the user. */ + readonly password!: string; + /** Additional permissions for the user. */ + readonly permissions!: string[]; + + constructor(data?: ICreateUserDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).email = _data["email"]; + (this).displayName = _data["displayName"]; + (this).password = _data["password"]; + if (Array.isArray(_data["permissions"])) { + (this).permissions = [] as any; + for (let item of _data["permissions"]) + (this).permissions!.push(item); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): CreateUserDto { + const result = new CreateUserDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["email"] = this.email; + data["displayName"] = this.displayName; + data["password"] = this.password; + if (Array.isArray(this.permissions)) { + data["permissions"] = []; + for (let item of this.permissions) + data["permissions"].push(item); + } + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface ICreateUserDto { + /** The email of the user. Unique value. */ + readonly email: string; + /** The display name (usually first name and last name) of the user. */ + readonly displayName: string; + /** The password of the user. */ + readonly password: string; + /** Additional permissions for the user. */ + readonly permissions: string[]; +} + +export class UpdateUserDto implements IUpdateUserDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The email of the user. Unique value. */ + readonly email!: string; + /** The display name (usually first name and last name) of the user. */ + readonly displayName!: string; + /** The password of the user. */ + readonly password?: string | undefined; + /** Additional permissions for the user. */ + readonly permissions!: string[]; + + constructor(data?: IUpdateUserDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).email = _data["email"]; + (this).displayName = _data["displayName"]; + (this).password = _data["password"]; + if (Array.isArray(_data["permissions"])) { + (this).permissions = [] as any; + for (let item of _data["permissions"]) + (this).permissions!.push(item); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): UpdateUserDto { + const result = new UpdateUserDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["email"] = this.email; + data["displayName"] = this.displayName; + data["password"] = this.password; + if (Array.isArray(this.permissions)) { + data["permissions"] = []; + for (let item of this.permissions) + data["permissions"].push(item); + } + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IUpdateUserDto { + /** The email of the user. Unique value. */ + readonly email: string; + /** The display name (usually first name and last name) of the user. */ + readonly displayName: string; + /** The password of the user. */ + readonly password?: string | undefined; + /** Additional permissions for the user. */ + readonly permissions: string[]; +} + +export class ResourcesDto extends ResourceDto implements IResourcesDto { + + constructor(data?: IResourcesDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + this.cleanup(this); + return this; + } + + static fromJSON(data: any): ResourcesDto { + const result = new ResourcesDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IResourcesDto extends IResourceDto { +} + +export class UpdateProfileDto implements IUpdateProfileDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The answers from a questionaire. */ + readonly answers?: { [key: string]: string; } | undefined; + + constructor(data?: IUpdateProfileDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + if (_data["answers"]) { + (this).answers = {} as any; + for (let key in _data["answers"]) { + if (_data["answers"].hasOwnProperty(key)) + ((this).answers)![key] = _data["answers"][key]; + } + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): UpdateProfileDto { + const result = new UpdateProfileDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (this.answers) { + data["answers"] = {}; + for (let key in this.answers) { + if (this.answers.hasOwnProperty(key)) + (data["answers"])[key] = (this.answers)[key]; + } + } + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IUpdateProfileDto { + /** The answers from a questionaire. */ + readonly answers?: { [key: string]: string; } | undefined; +} + +export class TranslationDto implements ITranslationDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The result of the translation. */ + readonly status!: TranslationStatus; + /** The result of the translation. */ + readonly result!: TranslationStatus; + /** The translated text. */ + readonly text?: string | undefined; + + constructor(data?: ITranslationDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).status = _data["status"]; + (this).result = _data["result"]; + (this).text = _data["text"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): TranslationDto { + const result = new TranslationDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["status"] = this.status; + data["result"] = this.result; + data["text"] = this.text; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface ITranslationDto { + /** The result of the translation. */ + readonly status: TranslationStatus; + /** The result of the translation. */ + readonly result: TranslationStatus; + /** The translated text. */ + readonly text?: string | undefined; +} + +export type TranslationStatus = "Translated" | "LanguageNotSupported" | "NotTranslated" | "NotConfigured" | "Unauthorized" | "Failed"; + +export const TranslationStatusValues: ReadonlyArray = [ + "Translated", + "LanguageNotSupported", + "NotTranslated", + "NotConfigured", + "Unauthorized", + "Failed" +]; + +export class TranslateDto implements ITranslateDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The text to translate. */ + readonly text!: string; + /** The target language. */ + readonly targetLanguage!: string; + /** The optional source language. */ + readonly sourceLanguage?: string | undefined; + + constructor(data?: ITranslateDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).text = _data["text"]; + (this).targetLanguage = _data["targetLanguage"]; + (this).sourceLanguage = _data["sourceLanguage"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): TranslateDto { + const result = new TranslateDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["text"] = this.text; + data["targetLanguage"] = this.targetLanguage; + data["sourceLanguage"] = this.sourceLanguage; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface ITranslateDto { + /** The text to translate. */ + readonly text: string; + /** The target language. */ + readonly targetLanguage: string; + /** The optional source language. */ + readonly sourceLanguage?: string | undefined; +} + +export class TemplatesDto extends ResourceDto implements ITemplatesDto { + /** The event consumers. */ + readonly items!: TemplateDto[]; + + constructor(data?: ITemplatesDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + if (Array.isArray(_data["items"])) { + (this).items = [] as any; + for (let item of _data["items"]) + (this).items!.push(TemplateDto.fromJSON(item)); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): TemplatesDto { + const result = new TemplatesDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (Array.isArray(this.items)) { + data["items"] = []; + for (let item of this.items) + data["items"].push(item.toJSON()); + } + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface ITemplatesDto extends IResourceDto { + /** The event consumers. */ + readonly items: TemplateDto[]; +} + +export class TemplateDto extends ResourceDto implements ITemplateDto { + /** The name of the template. */ + readonly name!: string; + /** The title of the template. */ + readonly title!: string; + /** The description of the template. */ + readonly description!: string; + /** True, if the template is a starter. */ + readonly isStarter!: boolean; + + constructor(data?: ITemplateDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).name = _data["name"]; + (this).title = _data["title"]; + (this).description = _data["description"]; + (this).isStarter = _data["isStarter"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): TemplateDto { + const result = new TemplateDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["name"] = this.name; + data["title"] = this.title; + data["description"] = this.description; + data["isStarter"] = this.isStarter; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface ITemplateDto extends IResourceDto { + /** The name of the template. */ + readonly name: string; + /** The title of the template. */ + readonly title: string; + /** The description of the template. */ + readonly description: string; + /** True, if the template is a starter. */ + readonly isStarter: boolean; +} + +export class TemplateDetailsDto extends ResourceDto implements ITemplateDetailsDto { + /** The details of the template. */ + readonly details!: string; + + constructor(data?: ITemplateDetailsDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).details = _data["details"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): TemplateDetailsDto { + const result = new TemplateDetailsDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["details"] = this.details; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface ITemplateDetailsDto extends IResourceDto { + /** The details of the template. */ + readonly details: string; +} + +export class ContributorsDto extends ResourceDto implements IContributorsDto { + /** The contributors. */ + readonly items!: ContributorDto[]; + /** The maximum number of allowed contributors. */ + readonly maxContributors!: number; + /** The metadata to provide information about this request. */ + readonly _meta?: ContributorsMetadataDto | undefined; + + get canCreate() { + return this.compute('canCreate', () => hasAnyLink(this._links, 'create')); + } + + get isInvited() { + return this.compute('isInvited', () => this._meta?.['isInvited'] === '1'); + } + + constructor(data?: IContributorsDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + if (Array.isArray(_data["items"])) { + (this).items = [] as any; + for (let item of _data["items"]) + (this).items!.push(ContributorDto.fromJSON(item)); + } + (this).maxContributors = _data["maxContributors"]; + (this)._meta = _data["_meta"] ? ContributorsMetadataDto.fromJSON(_data["_meta"]) : undefined; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): ContributorsDto { + const result = new ContributorsDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (Array.isArray(this.items)) { + data["items"] = []; + for (let item of this.items) + data["items"].push(item.toJSON()); + } + data["maxContributors"] = this.maxContributors; + data["_meta"] = this._meta ? this._meta.toJSON() : undefined; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IContributorsDto extends IResourceDto { + /** The contributors. */ + readonly items: ContributorDto[]; + /** The maximum number of allowed contributors. */ + readonly maxContributors: number; + /** The metadata to provide information about this request. */ + readonly _meta?: ContributorsMetadataDto | undefined; +} + +export class ContributorDto extends ResourceDto implements IContributorDto { + /** The ID of the user that contributes to the app. */ + readonly contributorId!: string; + /** The display name. */ + readonly contributorName!: string; + /** The email address. */ + readonly contributorEmail!: string; + /** The role of the contributor. */ + readonly role?: string | undefined; + + public get token() { + return `subject:${this.contributorId}`; + } + + get canRevoke() { + return this.compute('canRevoke ', () => hasAnyLink(this._links, 'update')); + } + + get canUpdate() { + return this.compute('canUpdate ', () => hasAnyLink(this._links, 'update')); + } + + constructor(data?: IContributorDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).contributorId = _data["contributorId"]; + (this).contributorName = _data["contributorName"]; + (this).contributorEmail = _data["contributorEmail"]; + (this).role = _data["role"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): ContributorDto { + const result = new ContributorDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["contributorId"] = this.contributorId; + data["contributorName"] = this.contributorName; + data["contributorEmail"] = this.contributorEmail; + data["role"] = this.role; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IContributorDto extends IResourceDto { + /** The ID of the user that contributes to the app. */ + readonly contributorId: string; + /** The display name. */ + readonly contributorName: string; + /** The email address. */ + readonly contributorEmail: string; + /** The role of the contributor. */ + readonly role?: string | undefined; +} + +export class ContributorsMetadataDto implements IContributorsMetadataDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** Indicates whether the user has been invited. */ + readonly isInvited!: string; + + constructor(data?: IContributorsMetadataDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).isInvited = _data["isInvited"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): ContributorsMetadataDto { + const result = new ContributorsMetadataDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["isInvited"] = this.isInvited; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IContributorsMetadataDto { + /** Indicates whether the user has been invited. */ + readonly isInvited: string; +} + +export class AssignContributorDto implements IAssignContributorDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The id or email of the user to add to the app. */ + readonly contributorId!: string; + /** The role of the contributor. */ + readonly role?: string | undefined; + /** Set to true to invite the user if he does not exist. */ + readonly invite?: boolean; + + constructor(data?: IAssignContributorDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).contributorId = _data["contributorId"]; + (this).role = _data["role"]; + (this).invite = _data["invite"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): AssignContributorDto { + const result = new AssignContributorDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["contributorId"] = this.contributorId; + data["role"] = this.role; + data["invite"] = this.invite; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IAssignContributorDto { + /** The id or email of the user to add to the app. */ + readonly contributorId: string; + /** The role of the contributor. */ + readonly role?: string | undefined; + /** Set to true to invite the user if he does not exist. */ + readonly invite?: boolean; +} + +export class TeamDto extends ResourceDto implements ITeamDto { + /** The user that has created the app. */ + readonly createdBy?: string | undefined; + /** The user that has updated the app. */ + readonly lastModifiedBy?: string | undefined; + /** The ID of the team. */ + readonly id!: string; + /** The name of the team. */ + readonly name!: string; + /** The version of the team. */ + readonly version!: number; + /** The timestamp when the team has been created. */ + readonly created!: DateTime; + /** The timestamp when the team has been modified last. */ + readonly lastModified!: DateTime; + /** The role name of the user. */ + readonly roleName?: string | undefined; + + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'delete')); + } + + get canReadAuth() { + return this.compute('canReadAuth', () => hasAnyLink(this._links, 'auth')); + } + + get canReadContributors() { + return this.compute('canReadContributors', () => hasAnyLink(this._links, 'contributors')); + } + + get canReadPlans() { + return this.compute('canReadPlans', () => hasAnyLink(this._links, 'plans')); + } + + get canUpdateGeneral() { + return this.compute('canUpdateGeneral', () => hasAnyLink(this._links, 'update')); + } + + constructor(data?: ITeamDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).createdBy = _data["createdBy"]; + (this).lastModifiedBy = _data["lastModifiedBy"]; + (this).id = _data["id"]; + (this).name = _data["name"]; + (this).version = _data["version"]; + (this).created = _data["created"] ? DateTime.parseISO(_data["created"].toString()) : undefined; + (this).lastModified = _data["lastModified"] ? DateTime.parseISO(_data["lastModified"].toString()) : undefined; + (this).roleName = _data["roleName"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): TeamDto { + const result = new TeamDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["createdBy"] = this.createdBy; + data["lastModifiedBy"] = this.lastModifiedBy; + data["id"] = this.id; + data["name"] = this.name; + data["version"] = this.version; + data["created"] = this.created ? this.created.toISOString() : undefined; + data["lastModified"] = this.lastModified ? this.lastModified.toISOString() : undefined; + data["roleName"] = this.roleName; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface ITeamDto extends IResourceDto { + /** The user that has created the app. */ + readonly createdBy?: string | undefined; + /** The user that has updated the app. */ + readonly lastModifiedBy?: string | undefined; + /** The ID of the team. */ + readonly id: string; + /** The name of the team. */ + readonly name: string; + /** The version of the team. */ + readonly version: number; + /** The timestamp when the team has been created. */ + readonly created: DateTime; + /** The timestamp when the team has been modified last. */ + readonly lastModified: DateTime; + /** The role name of the user. */ + readonly roleName?: string | undefined; +} + +export class CreateTeamDto implements ICreateTeamDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The name of the team. */ + readonly name!: string; + + constructor(data?: ICreateTeamDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).name = _data["name"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): CreateTeamDto { + const result = new CreateTeamDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["name"] = this.name; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface ICreateTeamDto { + /** The name of the team. */ + readonly name: string; +} + +export class UpdateTeamDto implements IUpdateTeamDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The name of the team. */ + readonly name!: string; + + constructor(data?: IUpdateTeamDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).name = _data["name"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): UpdateTeamDto { + const result = new UpdateTeamDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["name"] = this.name; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IUpdateTeamDto { + /** The name of the team. */ + readonly name: string; +} + +export class AuthSchemeResponseDto extends ResourceDto implements IAuthSchemeResponseDto { + /** The auth scheme if configured. */ + readonly scheme?: AuthSchemeDto | undefined; + + get canUpdate() { + return this.compute('canUpdate', () => hasAnyLink(this._links, 'update')); + } + + constructor(data?: IAuthSchemeResponseDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).scheme = _data["scheme"] ? AuthSchemeDto.fromJSON(_data["scheme"]) : undefined; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): AuthSchemeResponseDto { + const result = new AuthSchemeResponseDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["scheme"] = this.scheme ? this.scheme.toJSON() : undefined; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IAuthSchemeResponseDto extends IResourceDto { + /** The auth scheme if configured. */ + readonly scheme?: AuthSchemeDto | undefined; +} + +export class AuthSchemeDto implements IAuthSchemeDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The domain name of your user accounts. */ + readonly domain!: string; + /** The display name for buttons. */ + readonly displayName!: string; + /** The client ID. */ + readonly clientId!: string; + /** The client secret. */ + readonly clientSecret!: string; + /** The authority URL. */ + readonly authority!: string; + /** The URL to redirect after a signout. */ + readonly signoutRedirectUrl?: string | undefined; + + constructor(data?: IAuthSchemeDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).domain = _data["domain"]; + (this).displayName = _data["displayName"]; + (this).clientId = _data["clientId"]; + (this).clientSecret = _data["clientSecret"]; + (this).authority = _data["authority"]; + (this).signoutRedirectUrl = _data["signoutRedirectUrl"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): AuthSchemeDto { + const result = new AuthSchemeDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["domain"] = this.domain; + data["displayName"] = this.displayName; + data["clientId"] = this.clientId; + data["clientSecret"] = this.clientSecret; + data["authority"] = this.authority; + data["signoutRedirectUrl"] = this.signoutRedirectUrl; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IAuthSchemeDto { + /** The domain name of your user accounts. */ + readonly domain: string; + /** The display name for buttons. */ + readonly displayName: string; + /** The client ID. */ + readonly clientId: string; + /** The client secret. */ + readonly clientSecret: string; + /** The authority URL. */ + readonly authority: string; + /** The URL to redirect after a signout. */ + readonly signoutRedirectUrl?: string | undefined; +} + +export class AuthSchemeValueDto implements IAuthSchemeValueDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The auth scheme if configured. */ + readonly scheme?: AuthSchemeDto | undefined; + + constructor(data?: IAuthSchemeValueDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).scheme = _data["scheme"] ? AuthSchemeDto.fromJSON(_data["scheme"]) : undefined; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): AuthSchemeValueDto { + const result = new AuthSchemeValueDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["scheme"] = this.scheme ? this.scheme.toJSON() : undefined; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IAuthSchemeValueDto { + /** The auth scheme if configured. */ + readonly scheme?: AuthSchemeDto | undefined; +} + +export class LogDownloadDto implements ILogDownloadDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The url to download the log. */ + readonly downloadUrl?: string | undefined; + + constructor(data?: ILogDownloadDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).downloadUrl = _data["downloadUrl"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): LogDownloadDto { + const result = new LogDownloadDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["downloadUrl"] = this.downloadUrl; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface ILogDownloadDto { + /** The url to download the log. */ + readonly downloadUrl?: string | undefined; +} + +export class CallsUsageDto implements ICallsUsageDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The total number of API calls. */ + readonly totalCalls!: number; + /** The total number of bytes transferred. */ + readonly totalBytes!: number; + /** The total number of API calls this month. */ + readonly monthCalls!: number; + /** The total number of bytes transferred this month. */ + readonly monthBytes!: number; + /** The amount of calls that will block the app. */ + readonly blockingApiCalls!: number; + /** The included API traffic. */ + readonly allowedBytes!: number; + /** The included API calls. */ + readonly allowedCalls!: number; + /** The average duration in milliseconds. */ + readonly averageElapsedMs!: number; + /** The statistics by date and group. */ + readonly details!: { [key: string]: CallsUsagePerDateDto[]; }; + + constructor(data?: ICallsUsageDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).totalCalls = _data["totalCalls"]; + (this).totalBytes = _data["totalBytes"]; + (this).monthCalls = _data["monthCalls"]; + (this).monthBytes = _data["monthBytes"]; + (this).blockingApiCalls = _data["blockingApiCalls"]; + (this).allowedBytes = _data["allowedBytes"]; + (this).allowedCalls = _data["allowedCalls"]; + (this).averageElapsedMs = _data["averageElapsedMs"]; + if (_data["details"]) { + (this).details = {} as any; + for (let key in _data["details"]) { + if (_data["details"].hasOwnProperty(key)) + ((this).details)![key] = _data["details"][key] ? _data["details"][key].map((i: any) => CallsUsagePerDateDto.fromJSON(i)) : []; + } + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): CallsUsageDto { + const result = new CallsUsageDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["totalCalls"] = this.totalCalls; + data["totalBytes"] = this.totalBytes; + data["monthCalls"] = this.monthCalls; + data["monthBytes"] = this.monthBytes; + data["blockingApiCalls"] = this.blockingApiCalls; + data["allowedBytes"] = this.allowedBytes; + data["allowedCalls"] = this.allowedCalls; + data["averageElapsedMs"] = this.averageElapsedMs; + if (this.details) { + data["details"] = {}; + for (let key in this.details) { + if (this.details.hasOwnProperty(key)) + (data["details"])[key] = (this.details)[key]; + } + } + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface ICallsUsageDto { + /** The total number of API calls. */ + readonly totalCalls: number; + /** The total number of bytes transferred. */ + readonly totalBytes: number; + /** The total number of API calls this month. */ + readonly monthCalls: number; + /** The total number of bytes transferred this month. */ + readonly monthBytes: number; + /** The amount of calls that will block the app. */ + readonly blockingApiCalls: number; + /** The included API traffic. */ + readonly allowedBytes: number; + /** The included API calls. */ + readonly allowedCalls: number; + /** The average duration in milliseconds. */ + readonly averageElapsedMs: number; + /** The statistics by date and group. */ + readonly details: { [key: string]: CallsUsagePerDateDto[]; }; +} + +export class CallsUsagePerDateDto implements ICallsUsagePerDateDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The date when the usage was tracked. */ + readonly date!: DateTime; + /** The total number of API calls. */ + readonly totalCalls!: number; + /** The total number of bytes transferred. */ + readonly totalBytes!: number; + /** The average duration in milliseconds. */ + readonly averageElapsedMs!: number; + + constructor(data?: ICallsUsagePerDateDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).date = _data["date"] ? DateTime.parseISO(_data["date"].toString()) : undefined; + (this).totalCalls = _data["totalCalls"]; + (this).totalBytes = _data["totalBytes"]; + (this).averageElapsedMs = _data["averageElapsedMs"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): CallsUsagePerDateDto { + const result = new CallsUsagePerDateDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["date"] = this.date ? this.date.toISOString() : undefined; + data["totalCalls"] = this.totalCalls; + data["totalBytes"] = this.totalBytes; + data["averageElapsedMs"] = this.averageElapsedMs; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface ICallsUsagePerDateDto { + /** The date when the usage was tracked. */ + readonly date: DateTime; + /** The total number of API calls. */ + readonly totalCalls: number; + /** The total number of bytes transferred. */ + readonly totalBytes: number; + /** The average duration in milliseconds. */ + readonly averageElapsedMs: number; +} + +export class CurrentStorageDto implements ICurrentStorageDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The size in bytes. */ + readonly size!: number; + /** The maximum allowed asset size. */ + readonly maxAllowed!: number; + + constructor(data?: ICurrentStorageDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).size = _data["size"]; + (this).maxAllowed = _data["maxAllowed"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): CurrentStorageDto { + const result = new CurrentStorageDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["size"] = this.size; + data["maxAllowed"] = this.maxAllowed; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface ICurrentStorageDto { + /** The size in bytes. */ + readonly size: number; + /** The maximum allowed asset size. */ + readonly maxAllowed: number; +} + +export class StorageUsagePerDateDto implements IStorageUsagePerDateDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The date when the usage was tracked. */ + readonly date!: DateTime; + /** The number of assets. */ + readonly totalCount!: number; + /** The size in bytes. */ + readonly totalSize!: number; + + constructor(data?: IStorageUsagePerDateDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).date = _data["date"] ? DateTime.parseISO(_data["date"].toString()) : undefined; + (this).totalCount = _data["totalCount"]; + (this).totalSize = _data["totalSize"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): StorageUsagePerDateDto { + const result = new StorageUsagePerDateDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["date"] = this.date ? this.date.toISOString() : undefined; + data["totalCount"] = this.totalCount; + data["totalSize"] = this.totalSize; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IStorageUsagePerDateDto { + /** The date when the usage was tracked. */ + readonly date: DateTime; + /** The number of assets. */ + readonly totalCount: number; + /** The size in bytes. */ + readonly totalSize: number; +} + +export class SearchResultDto extends ResourceDto implements ISearchResultDto { + /** The name of the search result. */ + readonly name!: string; + /** The type of the search result. */ + readonly type!: SearchResultType; + /** An optional label. */ + readonly label?: string | undefined; + + get url() { + return this.compute('url', () => this._links['url'].href); + } + + constructor(data?: ISearchResultDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).name = _data["name"]; + (this).type = _data["type"]; + (this).label = _data["label"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): SearchResultDto { + const result = new SearchResultDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["name"] = this.name; + data["type"] = this.type; + data["label"] = this.label; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface ISearchResultDto extends IResourceDto { + /** The name of the search result. */ + readonly name: string; + /** The type of the search result. */ + readonly type: SearchResultType; + /** An optional label. */ + readonly label?: string | undefined; +} + +export type SearchResultType = "Asset" | "Content" | "Dashboard" | "Setting" | "Rule" | "Schema"; + +export const SearchResultTypeValues: ReadonlyArray = [ + "Asset", + "Content", + "Dashboard", + "Setting", + "Rule", + "Schema" +]; + +export class SchemaDto extends ResourceDto implements ISchemaDto { + /** The ID of the schema. */ + readonly id!: string; + /** The user that has created the schema. */ + readonly createdBy!: string; + /** The user that has updated the schema. */ + readonly lastModifiedBy!: string; + /** The name of the schema. Unique within the app. */ + readonly name!: string; + /** The type of the schema. */ + readonly type!: SchemaType; + /** The name of the category. */ + readonly category?: string | undefined; + /** The schema properties. */ + readonly properties!: SchemaPropertiesDto; + /** Indicates if the schema is a singleton. */ + readonly isSingleton!: boolean; + /** Indicates if the schema is published. */ + readonly isPublished!: boolean; + /** The date and time when the schema has been created. */ + readonly created!: DateTime; + /** The date and time when the schema has been modified last. */ + readonly lastModified!: DateTime; + /** The version of the schema. */ + readonly version!: number; + /** The scripts. */ + readonly scripts!: SchemaScriptsDto; + /** The preview Urls. */ + readonly previewUrls!: { [key: string]: string; }; + /** The name of fields that are used in content lists. */ + readonly fieldsInLists!: string[]; + /** The name of fields that are used in content references. */ + readonly fieldsInReferences!: string[]; + /** The field rules. */ + readonly fieldRules!: FieldRuleDto[]; + /** The list of fields. */ + readonly fields!: FieldDto[]; + + get canAddField() { + return this.compute('canAddField', () => hasAnyLink(this._links, 'fields/add')); + } + + get canContentsCreate() { + return this.compute('canContentsCreate', () => hasAnyLink(this._links, 'contents/create')); + } + + get canContentsCreateAndPublish() { + return this.compute('canContentsCreateAndPublish', () => hasAnyLink(this._links, 'contents/create/publish')); + } + + get canContentsRead() { + return this.compute('canContentsCreateAndPublish', () => hasAnyLink(this._links, 'contents')); + } + + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'delete')); + } + + get canOrderFields() { + return this.compute('canOrderFields', () => hasAnyLink(this._links, 'fields/order')); + } + + get canPublish() { + return this.compute('canPublish', () => hasAnyLink(this._links, 'publish')); + } + + get canReadContents() { + return this.compute('canReadContents', () => hasAnyLink(this._links, 'contents')); + } + + get canSynchronize() { + return this.compute('canReadContents', () => hasAnyLink(this._links, 'update/sync')); + } + + get canUnpublish() { + return this.compute('canReadContents', () => hasAnyLink(this._links, 'unpublish')); + } + + get canUpdate() { + return this.compute('canReadContents', () => hasAnyLink(this._links, 'update')); + } + + get canUpdateCategory() { + return this.compute('canUpdateCategory', () => hasAnyLink(this._links, 'update/category')); + } + + get canUpdateRules() { + return this.compute('canUpdateCategory', () => hasAnyLink(this._links, 'update/rules')); + } + + get canUpdateScripts() { + return this.compute('canUpdateScripts', () => hasAnyLink(this._links, 'update/scripts')); + } + + get canUpdateUIFields() { + return this.compute('canUpdateUIFields', () => hasAnyLink(this._links, 'fields/ui')); + } + + get canUpdateUrls() { + return this.compute('canUpdateUrls', () => hasAnyLink(this._links, 'update/urls')); + } + + get displayName() { + return this.compute('displayName', () => StringHelper.firstNonEmpty(this.properties.label, this.name)); + } + + get contentFields() { + return this.compute('displayName', () => this.fields.filter(x => x.properties.isContentField).map(tableField)); + } + + get defaultListFields() { + return this.compute('defaultListFields', () => { + const listFields = tableFields(this.fieldsInLists, this.contentFields); + + if (listFields.length === 0) { + listFields.push(META_FIELDS.lastModifiedByAvatar); + + if (this.fields.length > 0) { + listFields.push(tableField(this.fields[0])); + } else { + listFields.push(META_FIELDS.empty); + } + + listFields.push(META_FIELDS.statusColor); + listFields.push(META_FIELDS.lastModified); + } + + return listFields; + }); + } + + get defaultReferenceFields() { + return this.compute('defaultReferenceFields', () => { + const referenceFields = tableFields(this.fieldsInReferences, this.contentFields); + + if (referenceFields.length === 0) { + if (this.fields.length > 0) { + referenceFields.push(tableField(this.fields[0])); + } else { + referenceFields.push(META_FIELDS.empty); + } + } + + return referenceFields; + }); + } + + public export(): any { + const fieldKeys = [ + 'fieldId', + 'parentId', + 'parentFieldId', + '_links', + ]; + + const cleanup = (source: any, ...exclude: string[]): any => { + const clone = {} as Record; + + for (const [key, value] of Object.entries(source)) { + if (!exclude.includes(key) && key.indexOf('can') !== 0 && !Types.isUndefined(value) && !Types.isNull(value)) { + clone[key] = value; + } + } + + return clone; + }; + + const result: any = { + previewUrls: this.previewUrls, + properties: cleanup(this.properties), + category: this.category, + scripts: this.scripts, + isPublished: this.isPublished, + fieldRules: this.fieldRules, + fieldsInLists: this.fieldsInLists, + fieldsInReferences: this.fieldsInReferences, + fields: this.fields.map(field => { + const copy = cleanup(field, ...fieldKeys); + + copy.properties = cleanup(field.properties); + + if (Types.isArray(copy.nested)) { + if (copy.nested.length === 0) { + delete copy['nested']; + } else if (field.nested) { + copy.nested = field.nested.map(nestedField => { + const nestedCopy = cleanup(nestedField, ...fieldKeys); + + nestedCopy.properties = cleanup(nestedField.properties); + + return nestedCopy; + }); + } + } + + return copy; + }), + type: this.type, + }; + + return result; + } + + constructor(data?: ISchemaDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).id = _data["id"]; + (this).createdBy = _data["createdBy"]; + (this).lastModifiedBy = _data["lastModifiedBy"]; + (this).name = _data["name"]; + (this).type = _data["type"]; + (this).category = _data["category"]; + (this).properties = _data["properties"] ? SchemaPropertiesDto.fromJSON(_data["properties"]) : new SchemaPropertiesDto(); + (this).isSingleton = _data["isSingleton"]; + (this).isPublished = _data["isPublished"]; + (this).created = _data["created"] ? DateTime.parseISO(_data["created"].toString()) : undefined; + (this).lastModified = _data["lastModified"] ? DateTime.parseISO(_data["lastModified"].toString()) : undefined; + (this).version = _data["version"]; + (this).scripts = _data["scripts"] ? SchemaScriptsDto.fromJSON(_data["scripts"]) : new SchemaScriptsDto(); + if (_data["previewUrls"]) { + (this).previewUrls = {} as any; + for (let key in _data["previewUrls"]) { + if (_data["previewUrls"].hasOwnProperty(key)) + ((this).previewUrls)![key] = _data["previewUrls"][key]; + } + } + if (Array.isArray(_data["fieldsInLists"])) { + (this).fieldsInLists = [] as any; + for (let item of _data["fieldsInLists"]) + (this).fieldsInLists!.push(item); + } + if (Array.isArray(_data["fieldsInReferences"])) { + (this).fieldsInReferences = [] as any; + for (let item of _data["fieldsInReferences"]) + (this).fieldsInReferences!.push(item); + } + if (Array.isArray(_data["fieldRules"])) { + (this).fieldRules = [] as any; + for (let item of _data["fieldRules"]) + (this).fieldRules!.push(FieldRuleDto.fromJSON(item)); + } + if (Array.isArray(_data["fields"])) { + (this).fields = [] as any; + for (let item of _data["fields"]) + (this).fields!.push(FieldDto.fromJSON(item)); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): SchemaDto { + const result = new SchemaDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["id"] = this.id; + data["createdBy"] = this.createdBy; + data["lastModifiedBy"] = this.lastModifiedBy; + data["name"] = this.name; + data["type"] = this.type; + data["category"] = this.category; + data["properties"] = this.properties ? this.properties.toJSON() : undefined; + data["isSingleton"] = this.isSingleton; + data["isPublished"] = this.isPublished; + data["created"] = this.created ? this.created.toISOString() : undefined; + data["lastModified"] = this.lastModified ? this.lastModified.toISOString() : undefined; + data["version"] = this.version; + data["scripts"] = this.scripts ? this.scripts.toJSON() : undefined; + if (this.previewUrls) { + data["previewUrls"] = {}; + for (let key in this.previewUrls) { + if (this.previewUrls.hasOwnProperty(key)) + (data["previewUrls"])[key] = (this.previewUrls)[key]; + } + } + if (Array.isArray(this.fieldsInLists)) { + data["fieldsInLists"] = []; + for (let item of this.fieldsInLists) + data["fieldsInLists"].push(item); + } + if (Array.isArray(this.fieldsInReferences)) { + data["fieldsInReferences"] = []; + for (let item of this.fieldsInReferences) + data["fieldsInReferences"].push(item); + } + if (Array.isArray(this.fieldRules)) { + data["fieldRules"] = []; + for (let item of this.fieldRules) + data["fieldRules"].push(item.toJSON()); + } + if (Array.isArray(this.fields)) { + data["fields"] = []; + for (let item of this.fields) + data["fields"].push(item.toJSON()); + } + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface ISchemaDto extends IResourceDto { + /** The ID of the schema. */ + readonly id: string; + /** The user that has created the schema. */ + readonly createdBy: string; + /** The user that has updated the schema. */ + readonly lastModifiedBy: string; + /** The name of the schema. Unique within the app. */ + readonly name: string; + /** The type of the schema. */ + readonly type: SchemaType; + /** The name of the category. */ + readonly category?: string | undefined; + /** The schema properties. */ + readonly properties: SchemaPropertiesDto; + /** Indicates if the schema is a singleton. */ + readonly isSingleton: boolean; + /** Indicates if the schema is published. */ + readonly isPublished: boolean; + /** The date and time when the schema has been created. */ + readonly created: DateTime; + /** The date and time when the schema has been modified last. */ + readonly lastModified: DateTime; + /** The version of the schema. */ + readonly version: number; + /** The scripts. */ + readonly scripts: SchemaScriptsDto; + /** The preview Urls. */ + readonly previewUrls: { [key: string]: string; }; + /** The name of fields that are used in content lists. */ + readonly fieldsInLists: string[]; + /** The name of fields that are used in content references. */ + readonly fieldsInReferences: string[]; + /** The field rules. */ + readonly fieldRules: FieldRuleDto[]; + /** The list of fields. */ + readonly fields: FieldDto[]; +} + +export type SchemaType = "Default" | "Singleton" | "Component"; + +export const SchemaTypeValues: ReadonlyArray = [ + "Default", + "Singleton", + "Component" +]; + +export class SchemaPropertiesDto implements ISchemaPropertiesDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** Optional label for the editor. */ + readonly label?: string | undefined; + /** Hints to describe the schema. */ + readonly hints?: string | undefined; + /** The url to a the sidebar plugin for content lists. */ + readonly contentsSidebarUrl?: string | undefined; + /** The url to a the sidebar plugin for content items. */ + readonly contentSidebarUrl?: string | undefined; + /** The url to the editor plugin. */ + readonly contentEditorUrl?: string | undefined; + /** The url to the editor plugin. */ + readonly contentsEditorUrl?: string | undefined; + /** The url to the content list plugin. */ + readonly contentsListUrl?: string | undefined; + /** True to validate the content items on publish. */ + readonly validateOnPublish!: boolean; + /** Tags for automation processes. */ + readonly tags?: string[] | undefined; + + constructor(data?: ISchemaPropertiesDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).label = _data["label"]; + (this).hints = _data["hints"]; + (this).contentsSidebarUrl = _data["contentsSidebarUrl"]; + (this).contentSidebarUrl = _data["contentSidebarUrl"]; + (this).contentEditorUrl = _data["contentEditorUrl"]; + (this).contentsEditorUrl = _data["contentsEditorUrl"]; + (this).contentsListUrl = _data["contentsListUrl"]; + (this).validateOnPublish = _data["validateOnPublish"]; + if (Array.isArray(_data["tags"])) { + (this).tags = [] as any; + for (let item of _data["tags"]) + (this).tags!.push(item); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): SchemaPropertiesDto { + const result = new SchemaPropertiesDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["label"] = this.label; + data["hints"] = this.hints; + data["contentsSidebarUrl"] = this.contentsSidebarUrl; + data["contentSidebarUrl"] = this.contentSidebarUrl; + data["contentEditorUrl"] = this.contentEditorUrl; + data["contentsEditorUrl"] = this.contentsEditorUrl; + data["contentsListUrl"] = this.contentsListUrl; + data["validateOnPublish"] = this.validateOnPublish; + if (Array.isArray(this.tags)) { + data["tags"] = []; + for (let item of this.tags) + data["tags"].push(item); + } + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface ISchemaPropertiesDto { + /** Optional label for the editor. */ + readonly label?: string | undefined; + /** Hints to describe the schema. */ + readonly hints?: string | undefined; + /** The url to a the sidebar plugin for content lists. */ + readonly contentsSidebarUrl?: string | undefined; + /** The url to a the sidebar plugin for content items. */ + readonly contentSidebarUrl?: string | undefined; + /** The url to the editor plugin. */ + readonly contentEditorUrl?: string | undefined; + /** The url to the editor plugin. */ + readonly contentsEditorUrl?: string | undefined; + /** The url to the content list plugin. */ + readonly contentsListUrl?: string | undefined; + /** True to validate the content items on publish. */ + readonly validateOnPublish: boolean; + /** Tags for automation processes. */ + readonly tags?: string[] | undefined; +} + +export class SchemaScriptsDto implements ISchemaScriptsDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The script that is executed for each content when querying contents. */ + readonly query?: string | undefined; + /** The script that is executed for all contents when querying contents. */ + readonly queryPre?: string | undefined; + /** The script that is executed when creating a content. */ + readonly create?: string | undefined; + /** The script that is executed when updating a content. */ + readonly update?: string | undefined; + /** The script that is executed when deleting a content. */ + readonly delete?: string | undefined; + /** The script that is executed when change a content status. */ + readonly change?: string | undefined; + + constructor(data?: ISchemaScriptsDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).query = _data["query"]; + (this).queryPre = _data["queryPre"]; + (this).create = _data["create"]; + (this).update = _data["update"]; + (this).delete = _data["delete"]; + (this).change = _data["change"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): SchemaScriptsDto { + const result = new SchemaScriptsDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["query"] = this.query; + data["queryPre"] = this.queryPre; + data["create"] = this.create; + data["update"] = this.update; + data["delete"] = this.delete; + data["change"] = this.change; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface ISchemaScriptsDto { + /** The script that is executed for each content when querying contents. */ + readonly query?: string | undefined; + /** The script that is executed for all contents when querying contents. */ + readonly queryPre?: string | undefined; + /** The script that is executed when creating a content. */ + readonly create?: string | undefined; + /** The script that is executed when updating a content. */ + readonly update?: string | undefined; + /** The script that is executed when deleting a content. */ + readonly delete?: string | undefined; + /** The script that is executed when change a content status. */ + readonly change?: string | undefined; +} + +export class FieldRuleDto implements IFieldRuleDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The action to perform when the condition is met. */ + readonly action!: FieldRuleAction; + /** The field to update. */ + readonly field!: string; + /** The condition. */ + readonly condition?: string | undefined; + + constructor(data?: IFieldRuleDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).action = _data["action"]; + (this).field = _data["field"]; + (this).condition = _data["condition"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): FieldRuleDto { + const result = new FieldRuleDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["action"] = this.action; + data["field"] = this.field; + data["condition"] = this.condition; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IFieldRuleDto { + /** The action to perform when the condition is met. */ + readonly action: FieldRuleAction; + /** The field to update. */ + readonly field: string; + /** The condition. */ + readonly condition?: string | undefined; +} + +export type FieldRuleAction = "Disable" | "Hide" | "Require"; + +export const FieldRuleActionValues: ReadonlyArray = [ + "Disable", + "Hide", + "Require" +]; + +export class FieldDto extends ResourceDto implements IFieldDto { + /** The ID of the field. */ + readonly fieldId!: number; + /** The name of the field. Must be unique within the schema. */ + readonly name!: string; + /** Defines if the field is hidden. */ + readonly isHidden!: boolean; + /** Defines if the field is locked. */ + readonly isLocked!: boolean; + /** Defines if the field is disabled. */ + readonly isDisabled!: boolean; + /** Defines the partitioning of the field. */ + readonly partitioning!: string; + /** The field properties. */ + readonly properties!: FieldPropertiesDto; + /** The nested fields. */ + readonly nested?: NestedFieldDto[] | undefined; + + public get rawProperties(): any { + return this.properties; + } + + public get isInlineEditable(): boolean { + return this.compute('isInlineEditable', () => !this.isDisabled && this.rawProperties.inlineEditable === true); + } + + public get isInvariant(): boolean { + return this.compute('isInvariant', () => this.partitioning === 'invariant'); + } + + public get isLocalizable(): boolean { + return this.compute('isLocalizable', () => this.partitioning === 'language'); + } + + public get displayName() { + return this.compute('displayName', () => StringHelper.firstNonEmpty(this.properties.label, this.name)); + } + + public get displayPlaceholder() { + return this.compute('displayPlaceholder', () => this.properties.placeholder || ''); + } + + get canAddField() { + return this.compute('canAddField', () => hasAnyLink(this._links, 'fields/add')); + } + + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'delete')); + } + + get canDisable() { + return this.compute('canDisable', () => hasAnyLink(this._links, 'disable')); + } + + get canEnable() { + return this.compute('canEnable', () => hasAnyLink(this._links, 'enable')); + } + + get canOrderFields() { + return this.compute('canOrderFields', () => hasAnyLink(this._links, 'fields/order')); + } + + get canHide() { + return this.compute('canHide', () => hasAnyLink(this._links, 'hide')); + } + + get canLock() { + return this.compute('canLock', () => hasAnyLink(this._links, 'lock')); + } + + get canShow() { + return this.compute('canShow', () => hasAnyLink(this._links, 'show')); + } + + get canUpdate() { + return this.compute('canUpdate', () => hasAnyLink(this._links, 'update')); + } + + constructor(data?: IFieldDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).fieldId = _data["fieldId"]; + (this).name = _data["name"]; + (this).isHidden = _data["isHidden"]; + (this).isLocked = _data["isLocked"]; + (this).isDisabled = _data["isDisabled"]; + (this).partitioning = _data["partitioning"]; + (this).properties = _data["properties"] ? FieldPropertiesDto.fromJSON(_data["properties"]) : undefined; + if (Array.isArray(_data["nested"])) { + (this).nested = [] as any; + for (let item of _data["nested"]) + (this).nested!.push(NestedFieldDto.fromJSON(item)); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): FieldDto { + const result = new FieldDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["fieldId"] = this.fieldId; + data["name"] = this.name; + data["isHidden"] = this.isHidden; + data["isLocked"] = this.isLocked; + data["isDisabled"] = this.isDisabled; + data["partitioning"] = this.partitioning; + data["properties"] = this.properties ? this.properties.toJSON() : undefined; + if (Array.isArray(this.nested)) { + data["nested"] = []; + for (let item of this.nested) + data["nested"].push(item.toJSON()); + } + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IFieldDto extends IResourceDto { + /** The ID of the field. */ + readonly fieldId: number; + /** The name of the field. Must be unique within the schema. */ + readonly name: string; + /** Defines if the field is hidden. */ + readonly isHidden: boolean; + /** Defines if the field is locked. */ + readonly isLocked: boolean; + /** Defines if the field is disabled. */ + readonly isDisabled: boolean; + /** Defines the partitioning of the field. */ + readonly partitioning: string; + /** The field properties. */ + readonly properties: FieldPropertiesDto; + /** The nested fields. */ + readonly nested?: NestedFieldDto[] | undefined; +} + +export abstract class FieldPropertiesDto implements IFieldPropertiesDto { + /** The discriminator. */ + public readonly fieldType!: string; + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** Optional label for the editor. */ + readonly label?: string | undefined; + /** Hints to describe the field. */ + readonly hints?: string | undefined; + /** Placeholder to show when no value has been entered. */ + readonly placeholder?: string | undefined; + /** Indicates if the field is required. */ + readonly isRequired?: boolean; + /** Indicates if the field is required when publishing. */ + readonly isRequiredOnPublish?: boolean; + /** Indicates if the field should be rendered with half width only. */ + readonly isHalfWidth?: boolean; + /** Optional url to the editor. */ + readonly editorUrl?: string | undefined; + /** Tags for automation processes. */ + readonly tags?: string[] | undefined; + + public get isComplexUI() { + return true; + } + + public get isSortable() { + return true; + } + + public get isContentField() { + return true; + } + + public accept(_visitor: FieldPropertiesVisitor): T { + throw new Error('NOT IMPLEMENTED'); + } + + constructor(data?: IFieldPropertiesDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + (this).fieldType = "FieldPropertiesDto"; + } + + init(_data: any) { + (this).label = _data["label"]; + (this).hints = _data["hints"]; + (this).placeholder = _data["placeholder"]; + (this).isRequired = _data["isRequired"]; + (this).isRequiredOnPublish = _data["isRequiredOnPublish"]; + (this).isHalfWidth = _data["isHalfWidth"]; + (this).editorUrl = _data["editorUrl"]; + if (Array.isArray(_data["tags"])) { + (this).tags = [] as any; + for (let item of _data["tags"]) + (this).tags!.push(item); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): FieldPropertiesDto { + if (data["fieldType"] === "Array") { + return new ArrayFieldPropertiesDto().init(data); + } + if (data["fieldType"] === "Assets") { + return new AssetsFieldPropertiesDto().init(data); + } + if (data["fieldType"] === "Boolean") { + return new BooleanFieldPropertiesDto().init(data); + } + if (data["fieldType"] === "Component") { + return new ComponentFieldPropertiesDto().init(data); + } + if (data["fieldType"] === "Components") { + return new ComponentsFieldPropertiesDto().init(data); + } + if (data["fieldType"] === "DateTime") { + return new DateTimeFieldPropertiesDto().init(data); + } + if (data["fieldType"] === "Geolocation") { + return new GeolocationFieldPropertiesDto().init(data); + } + if (data["fieldType"] === "Json") { + return new JsonFieldPropertiesDto().init(data); + } + if (data["fieldType"] === "Number") { + return new NumberFieldPropertiesDto().init(data); + } + if (data["fieldType"] === "References") { + return new ReferencesFieldPropertiesDto().init(data); + } + if (data["fieldType"] === "RichText") { + return new RichTextFieldPropertiesDto().init(data); + } + if (data["fieldType"] === "String") { + return new StringFieldPropertiesDto().init(data); + } + if (data["fieldType"] === "Tags") { + return new TagsFieldPropertiesDto().init(data); + } + if (data["fieldType"] === "UI") { + return new UIFieldPropertiesDto().init(data); + } + throw new Error("The abstract class 'FieldPropertiesDto' cannot be instantiated."); + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["fieldType"] = this.fieldType; + data["label"] = this.label; + data["hints"] = this.hints; + data["placeholder"] = this.placeholder; + data["isRequired"] = this.isRequired; + data["isRequiredOnPublish"] = this.isRequiredOnPublish; + data["isHalfWidth"] = this.isHalfWidth; + data["editorUrl"] = this.editorUrl; + if (Array.isArray(this.tags)) { + data["tags"] = []; + for (let item of this.tags) + data["tags"].push(item); + } + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IFieldPropertiesDto { + /** Optional label for the editor. */ + readonly label?: string | undefined; + /** Hints to describe the field. */ + readonly hints?: string | undefined; + /** Placeholder to show when no value has been entered. */ + readonly placeholder?: string | undefined; + /** Indicates if the field is required. */ + readonly isRequired?: boolean; + /** Indicates if the field is required when publishing. */ + readonly isRequiredOnPublish?: boolean; + /** Indicates if the field should be rendered with half width only. */ + readonly isHalfWidth?: boolean; + /** Optional url to the editor. */ + readonly editorUrl?: string | undefined; + /** Tags for automation processes. */ + readonly tags?: string[] | undefined; +} + +export class ArrayFieldPropertiesDto extends FieldPropertiesDto implements IArrayFieldPropertiesDto { + /** The minimum allowed items for the field value. */ + readonly minItems?: number | undefined; + /** The maximum allowed items for the field value. */ + readonly maxItems?: number | undefined; + /** The calculated default value for the field value. */ + readonly calculatedDefaultValue?: ArrayCalculatedDefaultValue; + /** The fields that must be unique. */ + readonly uniqueFields?: string[] | undefined; + + public accept(visitor: FieldPropertiesVisitor): T { + return visitor.visitArray(this); + } + + constructor(data?: IArrayFieldPropertiesDto) { + super(data); + (this).fieldType = "Array"; + } + + init(_data: any) { + super.init(_data); + (this).minItems = _data["minItems"]; + (this).maxItems = _data["maxItems"]; + (this).calculatedDefaultValue = _data["calculatedDefaultValue"]; + if (Array.isArray(_data["uniqueFields"])) { + (this).uniqueFields = [] as any; + for (let item of _data["uniqueFields"]) + (this).uniqueFields!.push(item); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): ArrayFieldPropertiesDto { + const result = new ArrayFieldPropertiesDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["minItems"] = this.minItems; + data["maxItems"] = this.maxItems; + data["calculatedDefaultValue"] = this.calculatedDefaultValue; + if (Array.isArray(this.uniqueFields)) { + data["uniqueFields"] = []; + for (let item of this.uniqueFields) + data["uniqueFields"].push(item); + } + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IArrayFieldPropertiesDto extends IFieldPropertiesDto { + /** The minimum allowed items for the field value. */ + readonly minItems?: number | undefined; + /** The maximum allowed items for the field value. */ + readonly maxItems?: number | undefined; + /** The calculated default value for the field value. */ + readonly calculatedDefaultValue?: ArrayCalculatedDefaultValue; + /** The fields that must be unique. */ + readonly uniqueFields?: string[] | undefined; +} + +export type ArrayCalculatedDefaultValue = "EmptyArray" | "Null"; + +export const ArrayCalculatedDefaultValueValues: ReadonlyArray = [ + "EmptyArray", + "Null" +]; + +export class AssetsFieldPropertiesDto extends FieldPropertiesDto implements IAssetsFieldPropertiesDto { + /** The preview mode for the asset. */ + readonly previewMode?: AssetPreviewMode; + /** The language specific default value as a list of asset ids. */ + readonly defaultValues?: { [key: string]: string[]; } | undefined; + /** The default value as a list of asset ids. */ + readonly defaultValue?: string[] | undefined; + /** The initial id to the folder. */ + readonly folderId?: string | undefined; + /** The preview format. */ + readonly previewFormat?: string | undefined; + /** The minimum allowed items for the field value. */ + readonly minItems?: number | undefined; + /** The maximum allowed items for the field value. */ + readonly maxItems?: number | undefined; + /** The minimum file size in bytes. */ + readonly minSize?: number | undefined; + /** The maximum file size in bytes. */ + readonly maxSize?: number | undefined; + /** The minimum image width in pixels. */ + readonly minWidth?: number | undefined; + /** The maximum image width in pixels. */ + readonly maxWidth?: number | undefined; + /** The minimum image height in pixels. */ + readonly minHeight?: number | undefined; + /** The maximum image height in pixels. */ + readonly maxHeight?: number | undefined; + /** The image aspect width in pixels. */ + readonly aspectWidth?: number | undefined; + /** The image aspect height in pixels. */ + readonly aspectHeight?: number | undefined; + /** The expected type. */ + readonly expectedType?: AssetType | undefined; + /** True to resolve first asset in the content list. */ + readonly resolveFirst?: boolean; + /** True to resolve first image in the content list. */ + readonly mustBeImage?: boolean; + /** True to resolve first image in the content list. */ + readonly resolveImage?: boolean; + /** The allowed file extensions. */ + readonly allowedExtensions?: string[] | undefined; + /** True, if duplicate values are allowed. */ + readonly allowDuplicates?: boolean; + + public get isSortable() { + return false; + } + + public accept(visitor: FieldPropertiesVisitor): T { + return visitor.visitAssets(this); + } + + constructor(data?: IAssetsFieldPropertiesDto) { + super(data); + (this).fieldType = "Assets"; + } + + init(_data: any) { + super.init(_data); + (this).previewMode = _data["previewMode"]; + if (_data["defaultValues"]) { + (this).defaultValues = {} as any; + for (let key in _data["defaultValues"]) { + if (_data["defaultValues"].hasOwnProperty(key)) + ((this).defaultValues)![key] = _data["defaultValues"][key] !== undefined ? _data["defaultValues"][key] : []; + } + } + if (Array.isArray(_data["defaultValue"])) { + (this).defaultValue = [] as any; + for (let item of _data["defaultValue"]) + (this).defaultValue!.push(item); + } + (this).folderId = _data["folderId"]; + (this).previewFormat = _data["previewFormat"]; + (this).minItems = _data["minItems"]; + (this).maxItems = _data["maxItems"]; + (this).minSize = _data["minSize"]; + (this).maxSize = _data["maxSize"]; + (this).minWidth = _data["minWidth"]; + (this).maxWidth = _data["maxWidth"]; + (this).minHeight = _data["minHeight"]; + (this).maxHeight = _data["maxHeight"]; + (this).aspectWidth = _data["aspectWidth"]; + (this).aspectHeight = _data["aspectHeight"]; + (this).expectedType = _data["expectedType"]; + (this).resolveFirst = _data["resolveFirst"]; + (this).mustBeImage = _data["mustBeImage"]; + (this).resolveImage = _data["resolveImage"]; + if (Array.isArray(_data["allowedExtensions"])) { + (this).allowedExtensions = [] as any; + for (let item of _data["allowedExtensions"]) + (this).allowedExtensions!.push(item); + } + (this).allowDuplicates = _data["allowDuplicates"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): AssetsFieldPropertiesDto { + const result = new AssetsFieldPropertiesDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["previewMode"] = this.previewMode; + if (this.defaultValues) { + data["defaultValues"] = {}; + for (let key in this.defaultValues) { + if (this.defaultValues.hasOwnProperty(key)) + (data["defaultValues"])[key] = (this.defaultValues)[key]; + } + } + if (Array.isArray(this.defaultValue)) { + data["defaultValue"] = []; + for (let item of this.defaultValue) + data["defaultValue"].push(item); + } + data["folderId"] = this.folderId; + data["previewFormat"] = this.previewFormat; + data["minItems"] = this.minItems; + data["maxItems"] = this.maxItems; + data["minSize"] = this.minSize; + data["maxSize"] = this.maxSize; + data["minWidth"] = this.minWidth; + data["maxWidth"] = this.maxWidth; + data["minHeight"] = this.minHeight; + data["maxHeight"] = this.maxHeight; + data["aspectWidth"] = this.aspectWidth; + data["aspectHeight"] = this.aspectHeight; + data["expectedType"] = this.expectedType; + data["resolveFirst"] = this.resolveFirst; + data["mustBeImage"] = this.mustBeImage; + data["resolveImage"] = this.resolveImage; + if (Array.isArray(this.allowedExtensions)) { + data["allowedExtensions"] = []; + for (let item of this.allowedExtensions) + data["allowedExtensions"].push(item); + } + data["allowDuplicates"] = this.allowDuplicates; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IAssetsFieldPropertiesDto extends IFieldPropertiesDto { + /** The preview mode for the asset. */ + readonly previewMode?: AssetPreviewMode; + /** The language specific default value as a list of asset ids. */ + readonly defaultValues?: { [key: string]: string[]; } | undefined; + /** The default value as a list of asset ids. */ + readonly defaultValue?: string[] | undefined; + /** The initial id to the folder. */ + readonly folderId?: string | undefined; + /** The preview format. */ + readonly previewFormat?: string | undefined; + /** The minimum allowed items for the field value. */ + readonly minItems?: number | undefined; + /** The maximum allowed items for the field value. */ + readonly maxItems?: number | undefined; + /** The minimum file size in bytes. */ + readonly minSize?: number | undefined; + /** The maximum file size in bytes. */ + readonly maxSize?: number | undefined; + /** The minimum image width in pixels. */ + readonly minWidth?: number | undefined; + /** The maximum image width in pixels. */ + readonly maxWidth?: number | undefined; + /** The minimum image height in pixels. */ + readonly minHeight?: number | undefined; + /** The maximum image height in pixels. */ + readonly maxHeight?: number | undefined; + /** The image aspect width in pixels. */ + readonly aspectWidth?: number | undefined; + /** The image aspect height in pixels. */ + readonly aspectHeight?: number | undefined; + /** The expected type. */ + readonly expectedType?: AssetType | undefined; + /** True to resolve first asset in the content list. */ + readonly resolveFirst?: boolean; + /** True to resolve first image in the content list. */ + readonly mustBeImage?: boolean; + /** True to resolve first image in the content list. */ + readonly resolveImage?: boolean; + /** The allowed file extensions. */ + readonly allowedExtensions?: string[] | undefined; + /** True, if duplicate values are allowed. */ + readonly allowDuplicates?: boolean; +} + +export type AssetPreviewMode = "ImageAndFileName" | "Image" | "FileName"; + +export const AssetPreviewModeValues: ReadonlyArray = [ + "ImageAndFileName", + "Image", + "FileName" +]; + +export type AssetType = "Unknown" | "Image" | "Audio" | "Video"; + +export const AssetTypeValues: ReadonlyArray = [ + "Unknown", + "Image", + "Audio", + "Video" +]; + +export class BooleanFieldPropertiesDto extends FieldPropertiesDto implements IBooleanFieldPropertiesDto { + /** The language specific default value for the field value. */ + readonly defaultValues?: { [key: string]: boolean; } | undefined; + /** The default value for the field value. */ + readonly defaultValue?: boolean | undefined; + /** Indicates that the inline editor is enabled for this field. */ + readonly inlineEditable?: boolean; + /** The editor that is used to manage this field. */ + readonly editor?: BooleanFieldEditor; + + public get isComplexUI() { + return false; + } + + public accept(visitor: FieldPropertiesVisitor): T { + return visitor.visitBoolean(this); + } + + constructor(data?: IBooleanFieldPropertiesDto) { + super(data); + (this).fieldType = "Boolean"; + } + + init(_data: any) { + super.init(_data); + if (_data["defaultValues"]) { + (this).defaultValues = {} as any; + for (let key in _data["defaultValues"]) { + if (_data["defaultValues"].hasOwnProperty(key)) + ((this).defaultValues)![key] = _data["defaultValues"][key]; + } + } + (this).defaultValue = _data["defaultValue"]; + (this).inlineEditable = _data["inlineEditable"]; + (this).editor = _data["editor"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): BooleanFieldPropertiesDto { + const result = new BooleanFieldPropertiesDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (this.defaultValues) { + data["defaultValues"] = {}; + for (let key in this.defaultValues) { + if (this.defaultValues.hasOwnProperty(key)) + (data["defaultValues"])[key] = (this.defaultValues)[key]; + } + } + data["defaultValue"] = this.defaultValue; + data["inlineEditable"] = this.inlineEditable; + data["editor"] = this.editor; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IBooleanFieldPropertiesDto extends IFieldPropertiesDto { + /** The language specific default value for the field value. */ + readonly defaultValues?: { [key: string]: boolean; } | undefined; + /** The default value for the field value. */ + readonly defaultValue?: boolean | undefined; + /** Indicates that the inline editor is enabled for this field. */ + readonly inlineEditable?: boolean; + /** The editor that is used to manage this field. */ + readonly editor?: BooleanFieldEditor; +} + +export type BooleanFieldEditor = "Checkbox" | "Toggle"; + +export const BooleanFieldEditorValues: ReadonlyArray = [ + "Checkbox", + "Toggle" +]; + +export class ComponentFieldPropertiesDto extends FieldPropertiesDto implements IComponentFieldPropertiesDto { + /** The ID of the embedded schemas. */ + readonly schemaIds?: string[] | undefined; + + public get isComplexUI() { + return true; + } + + public accept(visitor: FieldPropertiesVisitor): T { + return visitor.visitComponent(this); + } + + constructor(data?: IComponentFieldPropertiesDto) { + super(data); + (this).fieldType = "Component"; + } + + init(_data: any) { + super.init(_data); + if (Array.isArray(_data["schemaIds"])) { + (this).schemaIds = [] as any; + for (let item of _data["schemaIds"]) + (this).schemaIds!.push(item); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): ComponentFieldPropertiesDto { + const result = new ComponentFieldPropertiesDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (Array.isArray(this.schemaIds)) { + data["schemaIds"] = []; + for (let item of this.schemaIds) + data["schemaIds"].push(item); + } + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IComponentFieldPropertiesDto extends IFieldPropertiesDto { + /** The ID of the embedded schemas. */ + readonly schemaIds?: string[] | undefined; +} + +export class ComponentsFieldPropertiesDto extends FieldPropertiesDto implements IComponentsFieldPropertiesDto { + /** The minimum allowed items for the field value. */ + readonly minItems?: number | undefined; + /** The maximum allowed items for the field value. */ + readonly maxItems?: number | undefined; + /** The calculated default value for the field value. */ + readonly calculatedDefaultValue?: ArrayCalculatedDefaultValue; + /** The ID of the embedded schemas. */ + readonly schemaIds?: string[] | undefined; + /** The fields that must be unique. */ + readonly uniqueFields?: string[] | undefined; + + public get isComplexUI() { + return true; + } + + public accept(visitor: FieldPropertiesVisitor): T { + return visitor.visitComponents(this); + } + + constructor(data?: IComponentsFieldPropertiesDto) { + super(data); + (this).fieldType = "Components"; + } + + init(_data: any) { + super.init(_data); + (this).minItems = _data["minItems"]; + (this).maxItems = _data["maxItems"]; + (this).calculatedDefaultValue = _data["calculatedDefaultValue"]; + if (Array.isArray(_data["schemaIds"])) { + (this).schemaIds = [] as any; + for (let item of _data["schemaIds"]) + (this).schemaIds!.push(item); + } + if (Array.isArray(_data["uniqueFields"])) { + (this).uniqueFields = [] as any; + for (let item of _data["uniqueFields"]) + (this).uniqueFields!.push(item); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): ComponentsFieldPropertiesDto { + const result = new ComponentsFieldPropertiesDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["minItems"] = this.minItems; + data["maxItems"] = this.maxItems; + data["calculatedDefaultValue"] = this.calculatedDefaultValue; + if (Array.isArray(this.schemaIds)) { + data["schemaIds"] = []; + for (let item of this.schemaIds) + data["schemaIds"].push(item); + } + if (Array.isArray(this.uniqueFields)) { + data["uniqueFields"] = []; + for (let item of this.uniqueFields) + data["uniqueFields"].push(item); + } + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IComponentsFieldPropertiesDto extends IFieldPropertiesDto { + /** The minimum allowed items for the field value. */ + readonly minItems?: number | undefined; + /** The maximum allowed items for the field value. */ + readonly maxItems?: number | undefined; + /** The calculated default value for the field value. */ + readonly calculatedDefaultValue?: ArrayCalculatedDefaultValue; + /** The ID of the embedded schemas. */ + readonly schemaIds?: string[] | undefined; + /** The fields that must be unique. */ + readonly uniqueFields?: string[] | undefined; +} + +export class DateTimeFieldPropertiesDto extends FieldPropertiesDto implements IDateTimeFieldPropertiesDto { + /** The language specific default value for the field value. */ + readonly defaultValues?: { [key: string]: DateTime; } | undefined; + /** The default value for the field value. */ + readonly defaultValue?: DateTime | undefined; + /** The maximum allowed value for the field value. */ + readonly maxValue?: DateTime | undefined; + /** The minimum allowed value for the field value. */ + readonly minValue?: DateTime | undefined; + /** The format pattern when displayed in the UI. */ + readonly format?: string | undefined; + /** The editor that is used to manage this field. */ + readonly editor?: DateTimeFieldEditor; + /** The calculated default value for the field value. */ + readonly calculatedDefaultValue?: DateTimeCalculatedDefaultValue | undefined; + + public get isComplexUI() { + return false; + } + + public accept(visitor: FieldPropertiesVisitor): T { + return visitor.visitDateTime(this); + } + + constructor(data?: IDateTimeFieldPropertiesDto) { + super(data); + (this).fieldType = "DateTime"; + } + + init(_data: any) { + super.init(_data); + if (_data["defaultValues"]) { + (this).defaultValues = {} as any; + for (let key in _data["defaultValues"]) { + if (_data["defaultValues"].hasOwnProperty(key)) + ((this).defaultValues)![key] = _data["defaultValues"][key] ? DateTime.parseISO(_data["defaultValues"][key].toString()) : undefined; + } + } + (this).defaultValue = _data["defaultValue"] ? DateTime.parseISO(_data["defaultValue"].toString()) : undefined; + (this).maxValue = _data["maxValue"] ? DateTime.parseISO(_data["maxValue"].toString()) : undefined; + (this).minValue = _data["minValue"] ? DateTime.parseISO(_data["minValue"].toString()) : undefined; + (this).format = _data["format"]; + (this).editor = _data["editor"]; + (this).calculatedDefaultValue = _data["calculatedDefaultValue"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): DateTimeFieldPropertiesDto { + const result = new DateTimeFieldPropertiesDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (this.defaultValues) { + data["defaultValues"] = {}; + for (let key in this.defaultValues) { + if (this.defaultValues.hasOwnProperty(key)) + (data["defaultValues"])[key] = this.defaultValues[key] ? this.defaultValues[key].toISOString() : undefined; + } + } + data["defaultValue"] = this.defaultValue ? this.defaultValue.toISOString() : undefined; + data["maxValue"] = this.maxValue ? this.maxValue.toISOString() : undefined; + data["minValue"] = this.minValue ? this.minValue.toISOString() : undefined; + data["format"] = this.format; + data["editor"] = this.editor; + data["calculatedDefaultValue"] = this.calculatedDefaultValue; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IDateTimeFieldPropertiesDto extends IFieldPropertiesDto { + /** The language specific default value for the field value. */ + readonly defaultValues?: { [key: string]: DateTime; } | undefined; + /** The default value for the field value. */ + readonly defaultValue?: DateTime | undefined; + /** The maximum allowed value for the field value. */ + readonly maxValue?: DateTime | undefined; + /** The minimum allowed value for the field value. */ + readonly minValue?: DateTime | undefined; + /** The format pattern when displayed in the UI. */ + readonly format?: string | undefined; + /** The editor that is used to manage this field. */ + readonly editor?: DateTimeFieldEditor; + /** The calculated default value for the field value. */ + readonly calculatedDefaultValue?: DateTimeCalculatedDefaultValue | undefined; +} + +export type DateTimeFieldEditor = "Date" | "DateTime"; + +export const DateTimeFieldEditorValues: ReadonlyArray = [ + "Date", + "DateTime" +]; + +export type DateTimeCalculatedDefaultValue = "Now" | "Today"; + +export const DateTimeCalculatedDefaultValueValues: ReadonlyArray = [ + "Now", + "Today" +]; + +export class GeolocationFieldPropertiesDto extends FieldPropertiesDto implements IGeolocationFieldPropertiesDto { + /** The editor that is used to manage this field. */ + readonly editor?: GeolocationFieldEditor; + + public get isSortable() { + return false; + } + + public accept(visitor: FieldPropertiesVisitor): T { + return visitor.visitGeolocation(this); + } + + constructor(data?: IGeolocationFieldPropertiesDto) { + super(data); + (this).fieldType = "Geolocation"; + } + + init(_data: any) { + super.init(_data); + (this).editor = _data["editor"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): GeolocationFieldPropertiesDto { + const result = new GeolocationFieldPropertiesDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["editor"] = this.editor; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IGeolocationFieldPropertiesDto extends IFieldPropertiesDto { + /** The editor that is used to manage this field. */ + readonly editor?: GeolocationFieldEditor; +} + +export type GeolocationFieldEditor = "Map"; + +export const GeolocationFieldEditorValues: ReadonlyArray = [ + "Map" +]; + +export class JsonFieldPropertiesDto extends FieldPropertiesDto implements IJsonFieldPropertiesDto { + /** The GraphQL schema. */ + readonly graphQLSchema?: string | undefined; + + public get isSortable() { + return false; + } + + public accept(visitor: FieldPropertiesVisitor): T { + return visitor.visitJson(this); + } + + constructor(data?: IJsonFieldPropertiesDto) { + super(data); + (this).fieldType = "Json"; + } + + init(_data: any) { + super.init(_data); + (this).graphQLSchema = _data["graphQLSchema"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): JsonFieldPropertiesDto { + const result = new JsonFieldPropertiesDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["graphQLSchema"] = this.graphQLSchema; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IJsonFieldPropertiesDto extends IFieldPropertiesDto { + /** The GraphQL schema. */ + readonly graphQLSchema?: string | undefined; +} + +export class NumberFieldPropertiesDto extends FieldPropertiesDto implements INumberFieldPropertiesDto { + /** The language specific default value for the field value. */ + readonly defaultValues?: { [key: string]: number; } | undefined; + /** The default value for the field value. */ + readonly defaultValue?: number | undefined; + /** The maximum allowed value for the field value. */ + readonly maxValue?: number | undefined; + /** The minimum allowed value for the field value. */ + readonly minValue?: number | undefined; + /** The allowed values for the field value. */ + readonly allowedValues?: number[] | undefined; + /** Indicates if the field value must be unique. Ignored for nested fields and localized fields. */ + readonly isUnique?: boolean; + /** Indicates that the inline editor is enabled for this field. */ + readonly inlineEditable?: boolean; + /** The editor that is used to manage this field. */ + readonly editor?: NumberFieldEditor; + + public get isComplexUI() { + return false; + } + + public accept(visitor: FieldPropertiesVisitor): T { + return visitor.visitNumber(this); + } + + constructor(data?: INumberFieldPropertiesDto) { + super(data); + (this).fieldType = "Number"; + } + + init(_data: any) { + super.init(_data); + if (_data["defaultValues"]) { + (this).defaultValues = {} as any; + for (let key in _data["defaultValues"]) { + if (_data["defaultValues"].hasOwnProperty(key)) + ((this).defaultValues)![key] = _data["defaultValues"][key]; + } + } + (this).defaultValue = _data["defaultValue"]; + (this).maxValue = _data["maxValue"]; + (this).minValue = _data["minValue"]; + if (Array.isArray(_data["allowedValues"])) { + (this).allowedValues = [] as any; + for (let item of _data["allowedValues"]) + (this).allowedValues!.push(item); + } + (this).isUnique = _data["isUnique"]; + (this).inlineEditable = _data["inlineEditable"]; + (this).editor = _data["editor"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): NumberFieldPropertiesDto { + const result = new NumberFieldPropertiesDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (this.defaultValues) { + data["defaultValues"] = {}; + for (let key in this.defaultValues) { + if (this.defaultValues.hasOwnProperty(key)) + (data["defaultValues"])[key] = (this.defaultValues)[key]; + } + } + data["defaultValue"] = this.defaultValue; + data["maxValue"] = this.maxValue; + data["minValue"] = this.minValue; + if (Array.isArray(this.allowedValues)) { + data["allowedValues"] = []; + for (let item of this.allowedValues) + data["allowedValues"].push(item); + } + data["isUnique"] = this.isUnique; + data["inlineEditable"] = this.inlineEditable; + data["editor"] = this.editor; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface INumberFieldPropertiesDto extends IFieldPropertiesDto { + /** The language specific default value for the field value. */ + readonly defaultValues?: { [key: string]: number; } | undefined; + /** The default value for the field value. */ + readonly defaultValue?: number | undefined; + /** The maximum allowed value for the field value. */ + readonly maxValue?: number | undefined; + /** The minimum allowed value for the field value. */ + readonly minValue?: number | undefined; + /** The allowed values for the field value. */ + readonly allowedValues?: number[] | undefined; + /** Indicates if the field value must be unique. Ignored for nested fields and localized fields. */ + readonly isUnique?: boolean; + /** Indicates that the inline editor is enabled for this field. */ + readonly inlineEditable?: boolean; + /** The editor that is used to manage this field. */ + readonly editor?: NumberFieldEditor; +} + +export type NumberFieldEditor = "Input" | "Radio" | "Dropdown" | "Stars"; + +export const NumberFieldEditorValues: ReadonlyArray = [ + "Input", + "Radio", + "Dropdown", + "Stars" +]; + +export class ReferencesFieldPropertiesDto extends FieldPropertiesDto implements IReferencesFieldPropertiesDto { + /** The language specific default value as a list of content ids. */ + readonly defaultValues?: { [key: string]: string[]; } | undefined; + /** The default value as a list of content ids. */ + readonly defaultValue?: string[] | undefined; + /** The minimum allowed items for the field value. */ + readonly minItems?: number | undefined; + /** The maximum allowed items for the field value. */ + readonly maxItems?: number | undefined; + /** True, if duplicate values are allowed. */ + readonly allowDuplicates?: boolean; + /** True to resolve references in the content list. */ + readonly resolveReference?: boolean; + /** True when all references must be published. */ + readonly mustBePublished?: boolean; + /** The initial query that is applied in the UI. */ + readonly query?: string | undefined; + /** The editor that is used to manage this field. */ + readonly editor?: ReferencesFieldEditor; + /** The ID of the referenced schemas. */ + readonly schemaIds?: string[] | undefined; + + public get singleId() { + return this.schemaIds?.[0] || null; + } + + public get isSortable() { + return false; + } + + public accept(visitor: FieldPropertiesVisitor): T { + return visitor.visitReferences(this); + } + + constructor(data?: IReferencesFieldPropertiesDto) { + super(data); + (this).fieldType = "References"; + } + + init(_data: any) { + super.init(_data); + if (_data["defaultValues"]) { + (this).defaultValues = {} as any; + for (let key in _data["defaultValues"]) { + if (_data["defaultValues"].hasOwnProperty(key)) + ((this).defaultValues)![key] = _data["defaultValues"][key] !== undefined ? _data["defaultValues"][key] : []; + } + } + if (Array.isArray(_data["defaultValue"])) { + (this).defaultValue = [] as any; + for (let item of _data["defaultValue"]) + (this).defaultValue!.push(item); + } + (this).minItems = _data["minItems"]; + (this).maxItems = _data["maxItems"]; + (this).allowDuplicates = _data["allowDuplicates"]; + (this).resolveReference = _data["resolveReference"]; + (this).mustBePublished = _data["mustBePublished"]; + (this).query = _data["query"]; + (this).editor = _data["editor"]; + if (Array.isArray(_data["schemaIds"])) { + (this).schemaIds = [] as any; + for (let item of _data["schemaIds"]) + (this).schemaIds!.push(item); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): ReferencesFieldPropertiesDto { + const result = new ReferencesFieldPropertiesDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (this.defaultValues) { + data["defaultValues"] = {}; + for (let key in this.defaultValues) { + if (this.defaultValues.hasOwnProperty(key)) + (data["defaultValues"])[key] = (this.defaultValues)[key]; + } + } + if (Array.isArray(this.defaultValue)) { + data["defaultValue"] = []; + for (let item of this.defaultValue) + data["defaultValue"].push(item); + } + data["minItems"] = this.minItems; + data["maxItems"] = this.maxItems; + data["allowDuplicates"] = this.allowDuplicates; + data["resolveReference"] = this.resolveReference; + data["mustBePublished"] = this.mustBePublished; + data["query"] = this.query; + data["editor"] = this.editor; + if (Array.isArray(this.schemaIds)) { + data["schemaIds"] = []; + for (let item of this.schemaIds) + data["schemaIds"].push(item); + } + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IReferencesFieldPropertiesDto extends IFieldPropertiesDto { + /** The language specific default value as a list of content ids. */ + readonly defaultValues?: { [key: string]: string[]; } | undefined; + /** The default value as a list of content ids. */ + readonly defaultValue?: string[] | undefined; + /** The minimum allowed items for the field value. */ + readonly minItems?: number | undefined; + /** The maximum allowed items for the field value. */ + readonly maxItems?: number | undefined; + /** True, if duplicate values are allowed. */ + readonly allowDuplicates?: boolean; + /** True to resolve references in the content list. */ + readonly resolveReference?: boolean; + /** True when all references must be published. */ + readonly mustBePublished?: boolean; + /** The initial query that is applied in the UI. */ + readonly query?: string | undefined; + /** The editor that is used to manage this field. */ + readonly editor?: ReferencesFieldEditor; + /** The ID of the referenced schemas. */ + readonly schemaIds?: string[] | undefined; +} + +export type ReferencesFieldEditor = "List" | "Dropdown" | "Tags" | "Checkboxes" | "Input" | "Radio"; + +export const ReferencesFieldEditorValues: ReadonlyArray = [ + "List", + "Dropdown", + "Tags", + "Checkboxes", + "Input", + "Radio" +]; + +export class RichTextFieldPropertiesDto extends FieldPropertiesDto implements IRichTextFieldPropertiesDto { + /** The initial id to the folder when the control supports file uploads. */ + readonly folderId?: string | undefined; + /** The minimum allowed length for the field value. */ + readonly minLength?: number | undefined; + /** The maximum allowed length for the field value. */ + readonly maxLength?: number | undefined; + /** The minimum allowed of normal characters for the field value. */ + readonly minCharacters?: number | undefined; + /** The maximum allowed of normal characters for the field value. */ + readonly maxCharacters?: number | undefined; + /** The minimum allowed number of words for the field value. */ + readonly minWords?: number | undefined; + /** The maximum allowed number of words for the field value. */ + readonly maxWords?: number | undefined; + /** The class names for the editor. */ + readonly classNames?: string[] | undefined; + /** The allowed schema ids that can be embedded. */ + readonly schemaIds?: string[] | undefined; + + public get isSortable() { + return false; + } + + public accept(visitor: FieldPropertiesVisitor): T { + return visitor.visitRichText(this); + } + + constructor(data?: IRichTextFieldPropertiesDto) { + super(data); + (this).fieldType = "RichText"; + } + + init(_data: any) { + super.init(_data); + (this).folderId = _data["folderId"]; + (this).minLength = _data["minLength"]; + (this).maxLength = _data["maxLength"]; + (this).minCharacters = _data["minCharacters"]; + (this).maxCharacters = _data["maxCharacters"]; + (this).minWords = _data["minWords"]; + (this).maxWords = _data["maxWords"]; + if (Array.isArray(_data["classNames"])) { + (this).classNames = [] as any; + for (let item of _data["classNames"]) + (this).classNames!.push(item); + } + if (Array.isArray(_data["schemaIds"])) { + (this).schemaIds = [] as any; + for (let item of _data["schemaIds"]) + (this).schemaIds!.push(item); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): RichTextFieldPropertiesDto { + const result = new RichTextFieldPropertiesDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["folderId"] = this.folderId; + data["minLength"] = this.minLength; + data["maxLength"] = this.maxLength; + data["minCharacters"] = this.minCharacters; + data["maxCharacters"] = this.maxCharacters; + data["minWords"] = this.minWords; + data["maxWords"] = this.maxWords; + if (Array.isArray(this.classNames)) { + data["classNames"] = []; + for (let item of this.classNames) + data["classNames"].push(item); + } + if (Array.isArray(this.schemaIds)) { + data["schemaIds"] = []; + for (let item of this.schemaIds) + data["schemaIds"].push(item); + } + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IRichTextFieldPropertiesDto extends IFieldPropertiesDto { + /** The initial id to the folder when the control supports file uploads. */ + readonly folderId?: string | undefined; + /** The minimum allowed length for the field value. */ + readonly minLength?: number | undefined; + /** The maximum allowed length for the field value. */ + readonly maxLength?: number | undefined; + /** The minimum allowed of normal characters for the field value. */ + readonly minCharacters?: number | undefined; + /** The maximum allowed of normal characters for the field value. */ + readonly maxCharacters?: number | undefined; + /** The minimum allowed number of words for the field value. */ + readonly minWords?: number | undefined; + /** The maximum allowed number of words for the field value. */ + readonly maxWords?: number | undefined; + /** The class names for the editor. */ + readonly classNames?: string[] | undefined; + /** The allowed schema ids that can be embedded. */ + readonly schemaIds?: string[] | undefined; +} + +export class StringFieldPropertiesDto extends FieldPropertiesDto implements IStringFieldPropertiesDto { + /** The language specific default value for the field value. */ + readonly defaultValues?: { [key: string]: string; } | undefined; + /** The default value for the field value. */ + readonly defaultValue?: string | undefined; + /** The pattern to enforce a specific format for the field value. */ + readonly pattern?: string | undefined; + /** The validation message for the pattern. */ + readonly patternMessage?: string | undefined; + /** The initial id to the folder when the control supports file uploads. */ + readonly folderId?: string | undefined; + /** The minimum allowed length for the field value. */ + readonly minLength?: number | undefined; + /** The maximum allowed length for the field value. */ + readonly maxLength?: number | undefined; + /** The minimum allowed of normal characters for the field value. */ + readonly minCharacters?: number | undefined; + /** The maximum allowed of normal characters for the field value. */ + readonly maxCharacters?: number | undefined; + /** The minimum allowed number of words for the field value. */ + readonly minWords?: number | undefined; + /** The maximum allowed number of words for the field value. */ + readonly maxWords?: number | undefined; + /** The class names for the editor. */ + readonly classNames?: string[] | undefined; + /** The allowed values for the field value. */ + readonly allowedValues?: string[] | undefined; + /** The allowed schema ids that can be embedded. */ + readonly schemaIds?: string[] | undefined; + /** Indicates if the field value must be unique. Ignored for nested fields and localized fields. */ + readonly isUnique?: boolean; + /** Indicates that other content items or references are embedded. */ + readonly isEmbeddable?: boolean; + /** Indicates that the inline editor is enabled for this field. */ + readonly inlineEditable?: boolean; + /** Indicates whether GraphQL Enum should be created. */ + readonly createEnum?: boolean; + /** How the string content should be interpreted. */ + readonly contentType?: StringContentType; + /** The editor that is used to manage this field. */ + readonly editor?: StringFieldEditor; + + public get isComplexUI() { + return this.editor !== 'Input' && this.editor !== 'Color' && this.editor !== 'Radio' && this.editor !== 'Slug' && this.editor !== 'TextArea'; + } + + public accept(visitor: FieldPropertiesVisitor): T { + return visitor.visitString(this); + } + + constructor(data?: IStringFieldPropertiesDto) { + super(data); + (this).fieldType = "String"; + } + + init(_data: any) { + super.init(_data); + if (_data["defaultValues"]) { + (this).defaultValues = {} as any; + for (let key in _data["defaultValues"]) { + if (_data["defaultValues"].hasOwnProperty(key)) + ((this).defaultValues)![key] = _data["defaultValues"][key]; + } + } + (this).defaultValue = _data["defaultValue"]; + (this).pattern = _data["pattern"]; + (this).patternMessage = _data["patternMessage"]; + (this).folderId = _data["folderId"]; + (this).minLength = _data["minLength"]; + (this).maxLength = _data["maxLength"]; + (this).minCharacters = _data["minCharacters"]; + (this).maxCharacters = _data["maxCharacters"]; + (this).minWords = _data["minWords"]; + (this).maxWords = _data["maxWords"]; + if (Array.isArray(_data["classNames"])) { + (this).classNames = [] as any; + for (let item of _data["classNames"]) + (this).classNames!.push(item); + } + if (Array.isArray(_data["allowedValues"])) { + (this).allowedValues = [] as any; + for (let item of _data["allowedValues"]) + (this).allowedValues!.push(item); + } + if (Array.isArray(_data["schemaIds"])) { + (this).schemaIds = [] as any; + for (let item of _data["schemaIds"]) + (this).schemaIds!.push(item); + } + (this).isUnique = _data["isUnique"]; + (this).isEmbeddable = _data["isEmbeddable"]; + (this).inlineEditable = _data["inlineEditable"]; + (this).createEnum = _data["createEnum"]; + (this).contentType = _data["contentType"]; + (this).editor = _data["editor"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): StringFieldPropertiesDto { + const result = new StringFieldPropertiesDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (this.defaultValues) { + data["defaultValues"] = {}; + for (let key in this.defaultValues) { + if (this.defaultValues.hasOwnProperty(key)) + (data["defaultValues"])[key] = (this.defaultValues)[key]; + } + } + data["defaultValue"] = this.defaultValue; + data["pattern"] = this.pattern; + data["patternMessage"] = this.patternMessage; + data["folderId"] = this.folderId; + data["minLength"] = this.minLength; + data["maxLength"] = this.maxLength; + data["minCharacters"] = this.minCharacters; + data["maxCharacters"] = this.maxCharacters; + data["minWords"] = this.minWords; + data["maxWords"] = this.maxWords; + if (Array.isArray(this.classNames)) { + data["classNames"] = []; + for (let item of this.classNames) + data["classNames"].push(item); + } + if (Array.isArray(this.allowedValues)) { + data["allowedValues"] = []; + for (let item of this.allowedValues) + data["allowedValues"].push(item); + } + if (Array.isArray(this.schemaIds)) { + data["schemaIds"] = []; + for (let item of this.schemaIds) + data["schemaIds"].push(item); + } + data["isUnique"] = this.isUnique; + data["isEmbeddable"] = this.isEmbeddable; + data["inlineEditable"] = this.inlineEditable; + data["createEnum"] = this.createEnum; + data["contentType"] = this.contentType; + data["editor"] = this.editor; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IStringFieldPropertiesDto extends IFieldPropertiesDto { + /** The language specific default value for the field value. */ + readonly defaultValues?: { [key: string]: string; } | undefined; + /** The default value for the field value. */ + readonly defaultValue?: string | undefined; + /** The pattern to enforce a specific format for the field value. */ + readonly pattern?: string | undefined; + /** The validation message for the pattern. */ + readonly patternMessage?: string | undefined; + /** The initial id to the folder when the control supports file uploads. */ + readonly folderId?: string | undefined; + /** The minimum allowed length for the field value. */ + readonly minLength?: number | undefined; + /** The maximum allowed length for the field value. */ + readonly maxLength?: number | undefined; + /** The minimum allowed of normal characters for the field value. */ + readonly minCharacters?: number | undefined; + /** The maximum allowed of normal characters for the field value. */ + readonly maxCharacters?: number | undefined; + /** The minimum allowed number of words for the field value. */ + readonly minWords?: number | undefined; + /** The maximum allowed number of words for the field value. */ + readonly maxWords?: number | undefined; + /** The class names for the editor. */ + readonly classNames?: string[] | undefined; + /** The allowed values for the field value. */ + readonly allowedValues?: string[] | undefined; + /** The allowed schema ids that can be embedded. */ + readonly schemaIds?: string[] | undefined; + /** Indicates if the field value must be unique. Ignored for nested fields and localized fields. */ + readonly isUnique?: boolean; + /** Indicates that other content items or references are embedded. */ + readonly isEmbeddable?: boolean; + /** Indicates that the inline editor is enabled for this field. */ + readonly inlineEditable?: boolean; + /** Indicates whether GraphQL Enum should be created. */ + readonly createEnum?: boolean; + /** How the string content should be interpreted. */ + readonly contentType?: StringContentType; + /** The editor that is used to manage this field. */ + readonly editor?: StringFieldEditor; +} + +export type StringContentType = "Unspecified" | "Html" | "Markdown"; + +export const StringContentTypeValues: ReadonlyArray = [ + "Unspecified", + "Html", + "Markdown" +]; + +export type StringFieldEditor = "Input" | "Color" | "Markdown" | "Dropdown" | "Html" | "Radio" | "RichText" | "Slug" | "StockPhoto" | "TextArea"; + +export const StringFieldEditorValues: ReadonlyArray = [ + "Input", + "Color", + "Markdown", + "Dropdown", + "Html", + "Radio", + "RichText", + "Slug", + "StockPhoto", + "TextArea" +]; + +export class TagsFieldPropertiesDto extends FieldPropertiesDto implements ITagsFieldPropertiesDto { + /** The language specific default value for the field value. */ + readonly defaultValues?: { [key: string]: string[]; } | undefined; + /** The default value. */ + readonly defaultValue?: string[] | undefined; + /** The minimum allowed items for the field value. */ + readonly minItems?: number | undefined; + /** The maximum allowed items for the field value. */ + readonly maxItems?: number | undefined; + /** The allowed values for the field value. */ + readonly allowedValues?: string[] | undefined; + /** Indicates whether GraphQL Enum should be created. */ + readonly createEnum?: boolean; + /** The editor that is used to manage this field. */ + readonly editor?: TagsFieldEditor; + + public get isComplexUI() { + return false; + } + + public get isSortable() { + return false; + } + + public accept(visitor: FieldPropertiesVisitor): T { + return visitor.visitTags(this); + } + + constructor(data?: ITagsFieldPropertiesDto) { + super(data); + (this).fieldType = "Tags"; + } + + init(_data: any) { + super.init(_data); + if (_data["defaultValues"]) { + (this).defaultValues = {} as any; + for (let key in _data["defaultValues"]) { + if (_data["defaultValues"].hasOwnProperty(key)) + ((this).defaultValues)![key] = _data["defaultValues"][key] !== undefined ? _data["defaultValues"][key] : []; + } + } + if (Array.isArray(_data["defaultValue"])) { + (this).defaultValue = [] as any; + for (let item of _data["defaultValue"]) + (this).defaultValue!.push(item); + } + (this).minItems = _data["minItems"]; + (this).maxItems = _data["maxItems"]; + if (Array.isArray(_data["allowedValues"])) { + (this).allowedValues = [] as any; + for (let item of _data["allowedValues"]) + (this).allowedValues!.push(item); + } + (this).createEnum = _data["createEnum"]; + (this).editor = _data["editor"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): TagsFieldPropertiesDto { + const result = new TagsFieldPropertiesDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (this.defaultValues) { + data["defaultValues"] = {}; + for (let key in this.defaultValues) { + if (this.defaultValues.hasOwnProperty(key)) + (data["defaultValues"])[key] = (this.defaultValues)[key]; + } + } + if (Array.isArray(this.defaultValue)) { + data["defaultValue"] = []; + for (let item of this.defaultValue) + data["defaultValue"].push(item); + } + data["minItems"] = this.minItems; + data["maxItems"] = this.maxItems; + if (Array.isArray(this.allowedValues)) { + data["allowedValues"] = []; + for (let item of this.allowedValues) + data["allowedValues"].push(item); + } + data["createEnum"] = this.createEnum; + data["editor"] = this.editor; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface ITagsFieldPropertiesDto extends IFieldPropertiesDto { + /** The language specific default value for the field value. */ + readonly defaultValues?: { [key: string]: string[]; } | undefined; + /** The default value. */ + readonly defaultValue?: string[] | undefined; + /** The minimum allowed items for the field value. */ + readonly minItems?: number | undefined; + /** The maximum allowed items for the field value. */ + readonly maxItems?: number | undefined; + /** The allowed values for the field value. */ + readonly allowedValues?: string[] | undefined; + /** Indicates whether GraphQL Enum should be created. */ + readonly createEnum?: boolean; + /** The editor that is used to manage this field. */ + readonly editor?: TagsFieldEditor; +} + +export type TagsFieldEditor = "Tags" | "Checkboxes" | "Dropdown"; + +export const TagsFieldEditorValues: ReadonlyArray = [ + "Tags", + "Checkboxes", + "Dropdown" +]; + +export class UIFieldPropertiesDto extends FieldPropertiesDto implements IUIFieldPropertiesDto { + /** The editor that is used to manage this field. */ + readonly editor?: UIFieldEditor; + + public get isComplexUI() { + return false; + } + + public get isSortable() { + return false; + } + + public get isContentField() { + return false; + } + + public accept(visitor: FieldPropertiesVisitor): T { + return visitor.visitUI(this); + } + + constructor(data?: IUIFieldPropertiesDto) { + super(data); + (this).fieldType = "UI"; + } + + init(_data: any) { + super.init(_data); + (this).editor = _data["editor"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): UIFieldPropertiesDto { + const result = new UIFieldPropertiesDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["editor"] = this.editor; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IUIFieldPropertiesDto extends IFieldPropertiesDto { + /** The editor that is used to manage this field. */ + readonly editor?: UIFieldEditor; +} + +export type UIFieldEditor = "Separator"; + +export const UIFieldEditorValues: ReadonlyArray = [ + "Separator" +]; + +export class NestedFieldDto extends ResourceDto implements INestedFieldDto { + /** The ID of the field. */ + readonly fieldId!: number; + /** The name of the field. Must be unique within the schema. */ + readonly name!: string; + /** Defines if the field is hidden. */ + readonly isHidden!: boolean; + /** Defines if the field is locked. */ + readonly isLocked!: boolean; + /** Defines if the field is disabled. */ + readonly isDisabled!: boolean; + /** The field properties. */ + readonly properties!: FieldPropertiesDto; + + public get rawProperties(): any { + return this.properties; + } + + public get displayName() { + return this.compute('displayName', () => StringHelper.firstNonEmpty(this.properties.label, this.name)); + } + + public get displayPlaceholder() { + return this.compute('displayPlaceholder', () => this.properties.placeholder || ''); + } + + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'delete')); + } + + get canDisable() { + return this.compute('canDisable', () => hasAnyLink(this._links, 'disable')); + } + + get canEnable() { + return this.compute('canEnable', () => hasAnyLink(this._links, 'enable')); + } + get canHide() { + return this.compute('canHide', () => hasAnyLink(this._links, 'hide')); + } + + get canLock() { + return this.compute('canLock', () => hasAnyLink(this._links, 'lock')); + } + + get canShow() { + return this.compute('canShow', () => hasAnyLink(this._links, 'show')); + } + + get canUpdate() { + return this.compute('canUpdate', () => hasAnyLink(this._links, 'update')); + } + + constructor(data?: INestedFieldDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).fieldId = _data["fieldId"]; + (this).name = _data["name"]; + (this).isHidden = _data["isHidden"]; + (this).isLocked = _data["isLocked"]; + (this).isDisabled = _data["isDisabled"]; + (this).properties = _data["properties"] ? FieldPropertiesDto.fromJSON(_data["properties"]) : undefined; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): NestedFieldDto { + const result = new NestedFieldDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["fieldId"] = this.fieldId; + data["name"] = this.name; + data["isHidden"] = this.isHidden; + data["isLocked"] = this.isLocked; + data["isDisabled"] = this.isDisabled; + data["properties"] = this.properties ? this.properties.toJSON() : undefined; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface INestedFieldDto extends IResourceDto { + /** The ID of the field. */ + readonly fieldId: number; + /** The name of the field. Must be unique within the schema. */ + readonly name: string; + /** Defines if the field is hidden. */ + readonly isHidden: boolean; + /** Defines if the field is locked. */ + readonly isLocked: boolean; + /** Defines if the field is disabled. */ + readonly isDisabled: boolean; + /** The field properties. */ + readonly properties: FieldPropertiesDto; +} + +export class AddFieldDto implements IAddFieldDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The name of the field. Must be unique within the schema. */ + readonly name!: string; + /** Determines the optional partitioning of the field. */ + readonly partitioning?: string | undefined; + /** The field properties. */ + readonly properties!: FieldPropertiesDto; + + constructor(data?: IAddFieldDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).name = _data["name"]; + (this).partitioning = _data["partitioning"]; + (this).properties = _data["properties"] ? FieldPropertiesDto.fromJSON(_data["properties"]) : undefined; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): AddFieldDto { + const result = new AddFieldDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["name"] = this.name; + data["partitioning"] = this.partitioning; + data["properties"] = this.properties ? this.properties.toJSON() : undefined; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IAddFieldDto { + /** The name of the field. Must be unique within the schema. */ + readonly name: string; + /** Determines the optional partitioning of the field. */ + readonly partitioning?: string | undefined; + /** The field properties. */ + readonly properties: FieldPropertiesDto; +} + +export class ConfigureUIFieldsDto implements IConfigureUIFieldsDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The name of fields that are used in content lists. */ + readonly fieldsInLists?: string[] | undefined; + /** The name of fields that are used in content references. */ + readonly fieldsInReferences?: string[] | undefined; + + constructor(data?: IConfigureUIFieldsDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + if (Array.isArray(_data["fieldsInLists"])) { + (this).fieldsInLists = [] as any; + for (let item of _data["fieldsInLists"]) + (this).fieldsInLists!.push(item); + } + if (Array.isArray(_data["fieldsInReferences"])) { + (this).fieldsInReferences = [] as any; + for (let item of _data["fieldsInReferences"]) + (this).fieldsInReferences!.push(item); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): ConfigureUIFieldsDto { + const result = new ConfigureUIFieldsDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (Array.isArray(this.fieldsInLists)) { + data["fieldsInLists"] = []; + for (let item of this.fieldsInLists) + data["fieldsInLists"].push(item); + } + if (Array.isArray(this.fieldsInReferences)) { + data["fieldsInReferences"] = []; + for (let item of this.fieldsInReferences) + data["fieldsInReferences"].push(item); + } + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IConfigureUIFieldsDto { + /** The name of fields that are used in content lists. */ + readonly fieldsInLists?: string[] | undefined; + /** The name of fields that are used in content references. */ + readonly fieldsInReferences?: string[] | undefined; +} + +export class ReorderFieldsDto implements IReorderFieldsDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The field ids in the target order. */ + readonly fieldIds!: number[]; + + constructor(data?: IReorderFieldsDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + if (Array.isArray(_data["fieldIds"])) { + (this).fieldIds = [] as any; + for (let item of _data["fieldIds"]) + (this).fieldIds!.push(item); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): ReorderFieldsDto { + const result = new ReorderFieldsDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (Array.isArray(this.fieldIds)) { + data["fieldIds"] = []; + for (let item of this.fieldIds) + data["fieldIds"].push(item); + } + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IReorderFieldsDto { + /** The field ids in the target order. */ + readonly fieldIds: number[]; +} + +export class UpdateFieldDto implements IUpdateFieldDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The field properties. */ + readonly properties!: FieldPropertiesDto; + + constructor(data?: IUpdateFieldDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).properties = _data["properties"] ? FieldPropertiesDto.fromJSON(_data["properties"]) : undefined; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): UpdateFieldDto { + const result = new UpdateFieldDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["properties"] = this.properties ? this.properties.toJSON() : undefined; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IUpdateFieldDto { + /** The field properties. */ + readonly properties: FieldPropertiesDto; +} + +export class IndexesDto extends ResourceDto implements IIndexesDto { + /** The indexes. */ + readonly items!: IndexDto[]; + + get canCreate() { + return this.compute('canCreate', () => hasAnyLink(this._links, 'create')); + } + + constructor(data?: IIndexesDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + if (Array.isArray(_data["items"])) { + (this).items = [] as any; + for (let item of _data["items"]) + (this).items!.push(IndexDto.fromJSON(item)); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): IndexesDto { + const result = new IndexesDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (Array.isArray(this.items)) { + data["items"] = []; + for (let item of this.items) + data["items"].push(item.toJSON()); + } + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IIndexesDto extends IResourceDto { + /** The indexes. */ + readonly items: IndexDto[]; +} + +export class IndexDto extends ResourceDto implements IIndexDto { + /** The name of the index. */ + readonly name!: string; + /** The index fields. */ + readonly fields!: IndexFieldDto[]; + + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'delete')); + } + + constructor(data?: IIndexDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).name = _data["name"]; + if (Array.isArray(_data["fields"])) { + (this).fields = [] as any; + for (let item of _data["fields"]) + (this).fields!.push(IndexFieldDto.fromJSON(item)); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): IndexDto { + const result = new IndexDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["name"] = this.name; + if (Array.isArray(this.fields)) { + data["fields"] = []; + for (let item of this.fields) + data["fields"].push(item.toJSON()); + } + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IIndexDto extends IResourceDto { + /** The name of the index. */ + readonly name: string; + /** The index fields. */ + readonly fields: IndexFieldDto[]; +} + +export class IndexFieldDto implements IIndexFieldDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The name of the field. */ + readonly name!: string; + /** The sort order of the field. */ + readonly order!: SortOrder; + + constructor(data?: IIndexFieldDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).name = _data["name"]; + (this).order = _data["order"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): IndexFieldDto { + const result = new IndexFieldDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["name"] = this.name; + data["order"] = this.order; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IIndexFieldDto { + /** The name of the field. */ + readonly name: string; + /** The sort order of the field. */ + readonly order: SortOrder; +} + +export type SortOrder = "Ascending" | "Descending"; + +export const SortOrderValues: ReadonlyArray = [ + "Ascending", + "Descending" +]; + +export class CreateIndexDto implements ICreateIndexDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The index fields. */ + readonly fields!: IndexFieldDto[]; + + constructor(data?: ICreateIndexDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + if (Array.isArray(_data["fields"])) { + (this).fields = [] as any; + for (let item of _data["fields"]) + (this).fields!.push(IndexFieldDto.fromJSON(item)); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): CreateIndexDto { + const result = new CreateIndexDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (Array.isArray(this.fields)) { + data["fields"] = []; + for (let item of this.fields) + data["fields"].push(item.toJSON()); + } + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface ICreateIndexDto { + /** The index fields. */ + readonly fields: IndexFieldDto[]; +} + +export class SchemasDto extends ResourceDto implements ISchemasDto { + /** The schemas. */ + readonly items!: SchemaDto[]; + + get canCreate() { + return this.compute('canCreate', () => hasAnyLink(this._links, 'create')); + } + + constructor(data?: ISchemasDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + if (Array.isArray(_data["items"])) { + (this).items = [] as any; + for (let item of _data["items"]) + (this).items!.push(SchemaDto.fromJSON(item)); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): SchemasDto { + const result = new SchemasDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (Array.isArray(this.items)) { + data["items"] = []; + for (let item of this.items) + data["items"].push(item.toJSON()); + } + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface ISchemasDto extends IResourceDto { + /** The schemas. */ + readonly items: SchemaDto[]; +} + +export abstract class UpsertSchemaDto implements IUpsertSchemaDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The optional properties. */ + readonly properties?: SchemaPropertiesDto | undefined; + /** The optional scripts. */ + readonly scripts?: SchemaScriptsDto | undefined; + /** The names of the fields that should be used in references. */ + readonly fieldsInReferences?: string[] | undefined; + /** The names of the fields that should be shown in lists, including meta fields. */ + readonly fieldsInLists?: string[] | undefined; + /** Optional fields. */ + readonly fields?: UpsertSchemaFieldDto[] | undefined; + /** The optional preview urls. */ + readonly previewUrls?: { [key: string]: string; } | undefined; + /** The optional field Rules. */ + readonly fieldRules?: FieldRuleDto[] | undefined; + /** The category. */ + readonly category?: string | undefined; + /** Set it to true to autopublish the schema. */ + readonly isPublished?: boolean; + + constructor(data?: IUpsertSchemaDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).properties = _data["properties"] ? SchemaPropertiesDto.fromJSON(_data["properties"]) : undefined; + (this).scripts = _data["scripts"] ? SchemaScriptsDto.fromJSON(_data["scripts"]) : undefined; + if (Array.isArray(_data["fieldsInReferences"])) { + (this).fieldsInReferences = [] as any; + for (let item of _data["fieldsInReferences"]) + (this).fieldsInReferences!.push(item); + } + if (Array.isArray(_data["fieldsInLists"])) { + (this).fieldsInLists = [] as any; + for (let item of _data["fieldsInLists"]) + (this).fieldsInLists!.push(item); + } + if (Array.isArray(_data["fields"])) { + (this).fields = [] as any; + for (let item of _data["fields"]) + (this).fields!.push(UpsertSchemaFieldDto.fromJSON(item)); + } + if (_data["previewUrls"]) { + (this).previewUrls = {} as any; + for (let key in _data["previewUrls"]) { + if (_data["previewUrls"].hasOwnProperty(key)) + ((this).previewUrls)![key] = _data["previewUrls"][key]; + } + } + if (Array.isArray(_data["fieldRules"])) { + (this).fieldRules = [] as any; + for (let item of _data["fieldRules"]) + (this).fieldRules!.push(FieldRuleDto.fromJSON(item)); + } + (this).category = _data["category"]; + (this).isPublished = _data["isPublished"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): UpsertSchemaDto { + throw new Error("The abstract class 'UpsertSchemaDto' cannot be instantiated."); + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["properties"] = this.properties ? this.properties.toJSON() : undefined; + data["scripts"] = this.scripts ? this.scripts.toJSON() : undefined; + if (Array.isArray(this.fieldsInReferences)) { + data["fieldsInReferences"] = []; + for (let item of this.fieldsInReferences) + data["fieldsInReferences"].push(item); + } + if (Array.isArray(this.fieldsInLists)) { + data["fieldsInLists"] = []; + for (let item of this.fieldsInLists) + data["fieldsInLists"].push(item); + } + if (Array.isArray(this.fields)) { + data["fields"] = []; + for (let item of this.fields) + data["fields"].push(item.toJSON()); + } + if (this.previewUrls) { + data["previewUrls"] = {}; + for (let key in this.previewUrls) { + if (this.previewUrls.hasOwnProperty(key)) + (data["previewUrls"])[key] = (this.previewUrls)[key]; + } + } + if (Array.isArray(this.fieldRules)) { + data["fieldRules"] = []; + for (let item of this.fieldRules) + data["fieldRules"].push(item.toJSON()); + } + data["category"] = this.category; + data["isPublished"] = this.isPublished; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IUpsertSchemaDto { + /** The optional properties. */ + readonly properties?: SchemaPropertiesDto | undefined; + /** The optional scripts. */ + readonly scripts?: SchemaScriptsDto | undefined; + /** The names of the fields that should be used in references. */ + readonly fieldsInReferences?: string[] | undefined; + /** The names of the fields that should be shown in lists, including meta fields. */ + readonly fieldsInLists?: string[] | undefined; + /** Optional fields. */ + readonly fields?: UpsertSchemaFieldDto[] | undefined; + /** The optional preview urls. */ + readonly previewUrls?: { [key: string]: string; } | undefined; + /** The optional field Rules. */ + readonly fieldRules?: FieldRuleDto[] | undefined; + /** The category. */ + readonly category?: string | undefined; + /** Set it to true to autopublish the schema. */ + readonly isPublished?: boolean; +} + +export class CreateSchemaDto extends UpsertSchemaDto implements ICreateSchemaDto { + /** The name of the schema. */ + readonly name!: string; + /** The type of the schema. */ + readonly type?: SchemaType; + /** Set to true to allow a single content item only. */ + readonly isSingleton?: boolean; + + constructor(data?: ICreateSchemaDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).name = _data["name"]; + (this).type = _data["type"]; + (this).isSingleton = _data["isSingleton"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): CreateSchemaDto { + const result = new CreateSchemaDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["name"] = this.name; + data["type"] = this.type; + data["isSingleton"] = this.isSingleton; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface ICreateSchemaDto extends IUpsertSchemaDto { + /** The name of the schema. */ + readonly name: string; + /** The type of the schema. */ + readonly type?: SchemaType; + /** Set to true to allow a single content item only. */ + readonly isSingleton?: boolean; +} + +export class UpsertSchemaFieldDto implements IUpsertSchemaFieldDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The name of the field. Must be unique within the schema. */ + readonly name!: string; + /** Defines if the field is hidden. */ + readonly isHidden?: boolean; + /** Defines if the field is locked. */ + readonly isLocked?: boolean; + /** Defines if the field is disabled. */ + readonly isDisabled?: boolean; + /** Determines the optional partitioning of the field. */ + readonly partitioning?: string | undefined; + /** The field properties. */ + readonly properties!: FieldPropertiesDto; + /** The nested fields. */ + readonly nested?: UpsertSchemaNestedFieldDto[] | undefined; + + constructor(data?: IUpsertSchemaFieldDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).name = _data["name"]; + (this).isHidden = _data["isHidden"]; + (this).isLocked = _data["isLocked"]; + (this).isDisabled = _data["isDisabled"]; + (this).partitioning = _data["partitioning"]; + (this).properties = _data["properties"] ? FieldPropertiesDto.fromJSON(_data["properties"]) : undefined; + if (Array.isArray(_data["nested"])) { + (this).nested = [] as any; + for (let item of _data["nested"]) + (this).nested!.push(UpsertSchemaNestedFieldDto.fromJSON(item)); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): UpsertSchemaFieldDto { + const result = new UpsertSchemaFieldDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["name"] = this.name; + data["isHidden"] = this.isHidden; + data["isLocked"] = this.isLocked; + data["isDisabled"] = this.isDisabled; + data["partitioning"] = this.partitioning; + data["properties"] = this.properties ? this.properties.toJSON() : undefined; + if (Array.isArray(this.nested)) { + data["nested"] = []; + for (let item of this.nested) + data["nested"].push(item.toJSON()); + } + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IUpsertSchemaFieldDto { + /** The name of the field. Must be unique within the schema. */ + readonly name: string; + /** Defines if the field is hidden. */ + readonly isHidden?: boolean; + /** Defines if the field is locked. */ + readonly isLocked?: boolean; + /** Defines if the field is disabled. */ + readonly isDisabled?: boolean; + /** Determines the optional partitioning of the field. */ + readonly partitioning?: string | undefined; + /** The field properties. */ + readonly properties: FieldPropertiesDto; + /** The nested fields. */ + readonly nested?: UpsertSchemaNestedFieldDto[] | undefined; +} + +export class UpsertSchemaNestedFieldDto implements IUpsertSchemaNestedFieldDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The name of the field. Must be unique within the schema. */ + readonly name!: string; + /** Defines if the field is hidden. */ + readonly isHidden?: boolean; + /** Defines if the field is locked. */ + readonly isLocked?: boolean; + /** Defines if the field is disabled. */ + readonly isDisabled?: boolean; + /** The field properties. */ + readonly properties!: FieldPropertiesDto; + + constructor(data?: IUpsertSchemaNestedFieldDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).name = _data["name"]; + (this).isHidden = _data["isHidden"]; + (this).isLocked = _data["isLocked"]; + (this).isDisabled = _data["isDisabled"]; + (this).properties = _data["properties"] ? FieldPropertiesDto.fromJSON(_data["properties"]) : undefined; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): UpsertSchemaNestedFieldDto { + const result = new UpsertSchemaNestedFieldDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["name"] = this.name; + data["isHidden"] = this.isHidden; + data["isLocked"] = this.isLocked; + data["isDisabled"] = this.isDisabled; + data["properties"] = this.properties ? this.properties.toJSON() : undefined; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IUpsertSchemaNestedFieldDto { + /** The name of the field. Must be unique within the schema. */ + readonly name: string; + /** Defines if the field is hidden. */ + readonly isHidden?: boolean; + /** Defines if the field is locked. */ + readonly isLocked?: boolean; + /** Defines if the field is disabled. */ + readonly isDisabled?: boolean; + /** The field properties. */ + readonly properties: FieldPropertiesDto; +} + +export class UpdateSchemaDto implements IUpdateSchemaDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** Optional label for the editor. */ + readonly label?: string | undefined; + /** Hints to describe the schema. */ + readonly hints?: string | undefined; + /** The url to a the sidebar plugin for content lists. */ + readonly contentsSidebarUrl?: string | undefined; + /** The url to a the sidebar plugin for content items. */ + readonly contentSidebarUrl?: string | undefined; + /** The url to the content list plugin. */ + readonly contentsListUrl?: string | undefined; + /** True to validate the content items on publish. */ + readonly validateOnPublish?: boolean; + /** Tags for automation processes. */ + readonly tags?: string[] | undefined; + + constructor(data?: IUpdateSchemaDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).label = _data["label"]; + (this).hints = _data["hints"]; + (this).contentsSidebarUrl = _data["contentsSidebarUrl"]; + (this).contentSidebarUrl = _data["contentSidebarUrl"]; + (this).contentsListUrl = _data["contentsListUrl"]; + (this).validateOnPublish = _data["validateOnPublish"]; + if (Array.isArray(_data["tags"])) { + (this).tags = [] as any; + for (let item of _data["tags"]) + (this).tags!.push(item); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): UpdateSchemaDto { + const result = new UpdateSchemaDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["label"] = this.label; + data["hints"] = this.hints; + data["contentsSidebarUrl"] = this.contentsSidebarUrl; + data["contentSidebarUrl"] = this.contentSidebarUrl; + data["contentsListUrl"] = this.contentsListUrl; + data["validateOnPublish"] = this.validateOnPublish; + if (Array.isArray(this.tags)) { + data["tags"] = []; + for (let item of this.tags) + data["tags"].push(item); + } + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IUpdateSchemaDto { + /** Optional label for the editor. */ + readonly label?: string | undefined; + /** Hints to describe the schema. */ + readonly hints?: string | undefined; + /** The url to a the sidebar plugin for content lists. */ + readonly contentsSidebarUrl?: string | undefined; + /** The url to a the sidebar plugin for content items. */ + readonly contentSidebarUrl?: string | undefined; + /** The url to the content list plugin. */ + readonly contentsListUrl?: string | undefined; + /** True to validate the content items on publish. */ + readonly validateOnPublish?: boolean; + /** Tags for automation processes. */ + readonly tags?: string[] | undefined; +} + +export class SynchronizeSchemaDto extends UpsertSchemaDto implements ISynchronizeSchemaDto { + /** True, when fields should not be deleted. */ + readonly noFieldDeletion?: boolean; + /** True, when fields with different types should not be recreated. */ + readonly noFieldRecreation?: boolean; + + constructor(data?: ISynchronizeSchemaDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).noFieldDeletion = _data["noFieldDeletion"]; + (this).noFieldRecreation = _data["noFieldRecreation"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): SynchronizeSchemaDto { + const result = new SynchronizeSchemaDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["noFieldDeletion"] = this.noFieldDeletion; + data["noFieldRecreation"] = this.noFieldRecreation; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface ISynchronizeSchemaDto extends IUpsertSchemaDto { + /** True, when fields should not be deleted. */ + readonly noFieldDeletion?: boolean; + /** True, when fields with different types should not be recreated. */ + readonly noFieldRecreation?: boolean; +} + +export class ChangeCategoryDto implements IChangeCategoryDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The name of the category. */ + readonly name?: string | undefined; + + constructor(data?: IChangeCategoryDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).name = _data["name"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): ChangeCategoryDto { + const result = new ChangeCategoryDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["name"] = this.name; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IChangeCategoryDto { + /** The name of the category. */ + readonly name?: string | undefined; +} + +export class ConfigureFieldRulesDto implements IConfigureFieldRulesDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The field rules to configure. */ + readonly fieldRules?: FieldRuleDto[] | undefined; + + constructor(data?: IConfigureFieldRulesDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + if (Array.isArray(_data["fieldRules"])) { + (this).fieldRules = [] as any; + for (let item of _data["fieldRules"]) + (this).fieldRules!.push(FieldRuleDto.fromJSON(item)); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): ConfigureFieldRulesDto { + const result = new ConfigureFieldRulesDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (Array.isArray(this.fieldRules)) { + data["fieldRules"] = []; + for (let item of this.fieldRules) + data["fieldRules"].push(item.toJSON()); + } + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IConfigureFieldRulesDto { + /** The field rules to configure. */ + readonly fieldRules?: FieldRuleDto[] | undefined; +} + +export class RuleElementDto implements IRuleElementDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** Describes the action or trigger type. */ + readonly description!: string; + /** The label for the action or trigger type. */ + readonly display!: string; + /** Optional title. */ + readonly title?: string | undefined; + /** The color for the icon. */ + readonly iconColor?: string | undefined; + /** The image for the icon. */ + readonly iconImage?: string | undefined; + /** The optional link to the product that is integrated. */ + readonly readMore?: string | undefined; + /** The properties. */ + readonly properties!: RuleElementPropertyDto[]; + + constructor(data?: IRuleElementDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).description = _data["description"]; + (this).display = _data["display"]; + (this).title = _data["title"]; + (this).iconColor = _data["iconColor"]; + (this).iconImage = _data["iconImage"]; + (this).readMore = _data["readMore"]; + if (Array.isArray(_data["properties"])) { + (this).properties = [] as any; + for (let item of _data["properties"]) + (this).properties!.push(RuleElementPropertyDto.fromJSON(item)); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): RuleElementDto { + const result = new RuleElementDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["description"] = this.description; + data["display"] = this.display; + data["title"] = this.title; + data["iconColor"] = this.iconColor; + data["iconImage"] = this.iconImage; + data["readMore"] = this.readMore; + if (Array.isArray(this.properties)) { + data["properties"] = []; + for (let item of this.properties) + data["properties"].push(item.toJSON()); + } + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IRuleElementDto { + /** Describes the action or trigger type. */ + readonly description: string; + /** The label for the action or trigger type. */ + readonly display: string; + /** Optional title. */ + readonly title?: string | undefined; + /** The color for the icon. */ + readonly iconColor?: string | undefined; + /** The image for the icon. */ + readonly iconImage?: string | undefined; + /** The optional link to the product that is integrated. */ + readonly readMore?: string | undefined; + /** The properties. */ + readonly properties: RuleElementPropertyDto[]; +} + +export class RuleElementPropertyDto implements IRuleElementPropertyDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The html editor. */ + readonly editor!: RuleFieldEditor; + /** The name of the editor. */ + readonly name!: string; + /** The label to use. */ + readonly display!: string; + /** The options, if the editor is a dropdown. */ + readonly options?: string[] | undefined; + /** The optional description. */ + readonly description?: string | undefined; + /** Indicates if the property is formattable. */ + readonly isFormattable!: boolean; + /** Indicates if the property is required. */ + readonly isRequired!: boolean; + + constructor(data?: IRuleElementPropertyDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).editor = _data["editor"]; + (this).name = _data["name"]; + (this).display = _data["display"]; + if (Array.isArray(_data["options"])) { + (this).options = [] as any; + for (let item of _data["options"]) + (this).options!.push(item); + } + (this).description = _data["description"]; + (this).isFormattable = _data["isFormattable"]; + (this).isRequired = _data["isRequired"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): RuleElementPropertyDto { + const result = new RuleElementPropertyDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["editor"] = this.editor; + data["name"] = this.name; + data["display"] = this.display; + if (Array.isArray(this.options)) { + data["options"] = []; + for (let item of this.options) + data["options"].push(item); + } + data["description"] = this.description; + data["isFormattable"] = this.isFormattable; + data["isRequired"] = this.isRequired; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IRuleElementPropertyDto { + /** The html editor. */ + readonly editor: RuleFieldEditor; + /** The name of the editor. */ + readonly name: string; + /** The label to use. */ + readonly display: string; + /** The options, if the editor is a dropdown. */ + readonly options?: string[] | undefined; + /** The optional description. */ + readonly description?: string | undefined; + /** Indicates if the property is formattable. */ + readonly isFormattable: boolean; + /** Indicates if the property is required. */ + readonly isRequired: boolean; +} + +export type RuleFieldEditor = "Checkbox" | "Dropdown" | "Email" | "Javascript" | "Number" | "Password" | "Text" | "TextArea" | "Url"; + +export const RuleFieldEditorValues: ReadonlyArray = [ + "Checkbox", + "Dropdown", + "Email", + "Javascript", + "Number", + "Password", + "Text", + "TextArea", + "Url" +]; + +export class RulesDto extends ResourceDto implements IRulesDto { + /** The rules. */ + readonly items!: RuleDto[]; + /** The ID of the rule that is currently rerunning. */ + readonly runningRuleId?: string | undefined; + + constructor(data?: IRulesDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + if (Array.isArray(_data["items"])) { + (this).items = [] as any; + for (let item of _data["items"]) + (this).items!.push(RuleDto.fromJSON(item)); + } + (this).runningRuleId = _data["runningRuleId"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): RulesDto { + const result = new RulesDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (Array.isArray(this.items)) { + data["items"] = []; + for (let item of this.items) + data["items"].push(item.toJSON()); + } + data["runningRuleId"] = this.runningRuleId; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IRulesDto extends IResourceDto { + /** The rules. */ + readonly items: RuleDto[]; + /** The ID of the rule that is currently rerunning. */ + readonly runningRuleId?: string | undefined; +} + +export class RuleDto extends ResourceDto implements IRuleDto { + /** The ID of the rule. */ + readonly id!: string; + /** The user that has created the rule. */ + readonly createdBy!: string; + /** The user that has updated the rule. */ + readonly lastModifiedBy!: string; + /** The date and time when the rule has been created. */ + readonly created!: DateTime; + /** The date and time when the rule has been modified last. */ + readonly lastModified!: DateTime; + /** The version of the rule. */ + readonly version!: number; + /** Determines if the rule is enabled. */ + readonly isEnabled!: boolean; + /** Optional rule name. */ + readonly name?: string | undefined; + /** The trigger properties. */ + readonly trigger!: RuleTriggerDto; + /** The action properties. */ + readonly action!: RuleActionDto; + /** The number of completed executions. */ + readonly numSucceeded!: number; + /** The number of failed executions. */ + readonly numFailed!: number; + /** The date and time when the rule was executed the last time. */ + readonly lastExecuted?: DateTime | undefined; + + constructor(data?: IRuleDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).id = _data["id"]; + (this).createdBy = _data["createdBy"]; + (this).lastModifiedBy = _data["lastModifiedBy"]; + (this).created = _data["created"] ? DateTime.parseISO(_data["created"].toString()) : undefined; + (this).lastModified = _data["lastModified"] ? DateTime.parseISO(_data["lastModified"].toString()) : undefined; + (this).version = _data["version"]; + (this).isEnabled = _data["isEnabled"]; + (this).name = _data["name"]; + (this).trigger = _data["trigger"] ? RuleTriggerDto.fromJSON(_data["trigger"]) : undefined; + (this).action = _data["action"] ? RuleActionDto.fromJSON(_data["action"]) : undefined; + (this).numSucceeded = _data["numSucceeded"]; + (this).numFailed = _data["numFailed"]; + (this).lastExecuted = _data["lastExecuted"] ? DateTime.parseISO(_data["lastExecuted"].toString()) : undefined; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): RuleDto { + const result = new RuleDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["id"] = this.id; + data["createdBy"] = this.createdBy; + data["lastModifiedBy"] = this.lastModifiedBy; + data["created"] = this.created ? this.created.toISOString() : undefined; + data["lastModified"] = this.lastModified ? this.lastModified.toISOString() : undefined; + data["version"] = this.version; + data["isEnabled"] = this.isEnabled; + data["name"] = this.name; + data["trigger"] = this.trigger ? this.trigger.toJSON() : undefined; + data["action"] = this.action ? this.action.toJSON() : undefined; + data["numSucceeded"] = this.numSucceeded; + data["numFailed"] = this.numFailed; + data["lastExecuted"] = this.lastExecuted ? this.lastExecuted.toISOString() : undefined; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IRuleDto extends IResourceDto { + /** The ID of the rule. */ + readonly id: string; + /** The user that has created the rule. */ + readonly createdBy: string; + /** The user that has updated the rule. */ + readonly lastModifiedBy: string; + /** The date and time when the rule has been created. */ + readonly created: DateTime; + /** The date and time when the rule has been modified last. */ + readonly lastModified: DateTime; + /** The version of the rule. */ + readonly version: number; + /** Determines if the rule is enabled. */ + readonly isEnabled: boolean; + /** Optional rule name. */ + readonly name?: string | undefined; + /** The trigger properties. */ + readonly trigger: RuleTriggerDto; + /** The action properties. */ + readonly action: RuleActionDto; + /** The number of completed executions. */ + readonly numSucceeded: number; + /** The number of failed executions. */ + readonly numFailed: number; + /** The date and time when the rule was executed the last time. */ + readonly lastExecuted?: DateTime | undefined; +} + +export abstract class RuleTriggerDto implements IRuleTriggerDto { + /** The discriminator. */ + public readonly triggerType!: string; + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + + constructor(data?: IRuleTriggerDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + (this).triggerType = "RuleTriggerDto"; + } + + init(_data: any) { + this.cleanup(this); + return this; + } + + static fromJSON(data: any): RuleTriggerDto { + if (data["triggerType"] === "AssetChanged") { + return new AssetChangedRuleTriggerDto().init(data); + } + if (data["triggerType"] === "Comment") { + return new CommentRuleTriggerDto().init(data); + } + if (data["triggerType"] === "ContentChanged") { + return new ContentChangedRuleTriggerDto().init(data); + } + if (data["triggerType"] === "Manual") { + return new ManualRuleTriggerDto().init(data); + } + if (data["triggerType"] === "SchemaChanged") { + return new SchemaChangedRuleTriggerDto().init(data); + } + if (data["triggerType"] === "Usage") { + return new UsageRuleTriggerDto().init(data); + } + throw new Error("The abstract class 'RuleTriggerDto' cannot be instantiated."); + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["triggerType"] = this.triggerType; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IRuleTriggerDto { +} + +export class AssetChangedRuleTriggerDto extends RuleTriggerDto implements IAssetChangedRuleTriggerDto { + /** Javascript condition when to trigger. */ + readonly condition?: string | undefined; + + constructor(data?: IAssetChangedRuleTriggerDto) { + super(data); + (this).triggerType = "AssetChanged"; + } + + init(_data: any) { + super.init(_data); + (this).condition = _data["condition"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): AssetChangedRuleTriggerDto { + const result = new AssetChangedRuleTriggerDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["condition"] = this.condition; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IAssetChangedRuleTriggerDto extends IRuleTriggerDto { + /** Javascript condition when to trigger. */ + readonly condition?: string | undefined; +} + +export class CommentRuleTriggerDto extends RuleTriggerDto implements ICommentRuleTriggerDto { + /** Javascript condition when to trigger. */ + readonly condition?: string | undefined; + + constructor(data?: ICommentRuleTriggerDto) { + super(data); + (this).triggerType = "Comment"; + } + + init(_data: any) { + super.init(_data); + (this).condition = _data["condition"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): CommentRuleTriggerDto { + const result = new CommentRuleTriggerDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["condition"] = this.condition; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface ICommentRuleTriggerDto extends IRuleTriggerDto { + /** Javascript condition when to trigger. */ + readonly condition?: string | undefined; +} + +export class ContentChangedRuleTriggerDto extends RuleTriggerDto implements IContentChangedRuleTriggerDto { + /** The schema settings. */ + readonly schemas?: SchemaConditionDto[] | undefined; + /** The schema references. */ + readonly referencedSchemas?: SchemaConditionDto[] | undefined; + /** Determines whether the trigger should handle all content changes events. */ + readonly handleAll!: boolean; + + constructor(data?: IContentChangedRuleTriggerDto) { + super(data); + (this).triggerType = "ContentChanged"; + } + + init(_data: any) { + super.init(_data); + if (Array.isArray(_data["schemas"])) { + (this).schemas = [] as any; + for (let item of _data["schemas"]) + (this).schemas!.push(SchemaConditionDto.fromJSON(item)); + } + if (Array.isArray(_data["referencedSchemas"])) { + (this).referencedSchemas = [] as any; + for (let item of _data["referencedSchemas"]) + (this).referencedSchemas!.push(SchemaConditionDto.fromJSON(item)); + } + (this).handleAll = _data["handleAll"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): ContentChangedRuleTriggerDto { + const result = new ContentChangedRuleTriggerDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (Array.isArray(this.schemas)) { + data["schemas"] = []; + for (let item of this.schemas) + data["schemas"].push(item.toJSON()); + } + if (Array.isArray(this.referencedSchemas)) { + data["referencedSchemas"] = []; + for (let item of this.referencedSchemas) + data["referencedSchemas"].push(item.toJSON()); + } + data["handleAll"] = this.handleAll; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IContentChangedRuleTriggerDto extends IRuleTriggerDto { + /** The schema settings. */ + readonly schemas?: SchemaConditionDto[] | undefined; + /** The schema references. */ + readonly referencedSchemas?: SchemaConditionDto[] | undefined; + /** Determines whether the trigger should handle all content changes events. */ + readonly handleAll: boolean; +} + +export class SchemaConditionDto implements ISchemaConditionDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + readonly schemaId!: string; + readonly condition?: string | undefined; + + constructor(data?: ISchemaConditionDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).schemaId = _data["schemaId"]; + (this).condition = _data["condition"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): SchemaConditionDto { + const result = new SchemaConditionDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["schemaId"] = this.schemaId; + data["condition"] = this.condition; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface ISchemaConditionDto { + readonly schemaId: string; + readonly condition?: string | undefined; +} + +export class ManualRuleTriggerDto extends RuleTriggerDto implements IManualRuleTriggerDto { + + constructor(data?: IManualRuleTriggerDto) { + super(data); + (this).triggerType = "Manual"; + } + + init(_data: any) { + super.init(_data); + this.cleanup(this); + return this; + } + + static fromJSON(data: any): ManualRuleTriggerDto { + const result = new ManualRuleTriggerDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IManualRuleTriggerDto extends IRuleTriggerDto { +} + +export class SchemaChangedRuleTriggerDto extends RuleTriggerDto implements ISchemaChangedRuleTriggerDto { + /** Javascript condition when to trigger. */ + readonly condition?: string | undefined; + + constructor(data?: ISchemaChangedRuleTriggerDto) { + super(data); + (this).triggerType = "SchemaChanged"; + } + + init(_data: any) { + super.init(_data); + (this).condition = _data["condition"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): SchemaChangedRuleTriggerDto { + const result = new SchemaChangedRuleTriggerDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["condition"] = this.condition; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface ISchemaChangedRuleTriggerDto extends IRuleTriggerDto { + /** Javascript condition when to trigger. */ + readonly condition?: string | undefined; +} + +export class UsageRuleTriggerDto extends RuleTriggerDto implements IUsageRuleTriggerDto { + /** The number of monthly api calls. */ + readonly limit!: number; + /** The number of days to check or null for the current month. */ + readonly numDays?: number | undefined; + + constructor(data?: IUsageRuleTriggerDto) { + super(data); + (this).triggerType = "Usage"; + } + + init(_data: any) { + super.init(_data); + (this).limit = _data["limit"]; + (this).numDays = _data["numDays"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): UsageRuleTriggerDto { + const result = new UsageRuleTriggerDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["limit"] = this.limit; + data["numDays"] = this.numDays; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IUsageRuleTriggerDto extends IRuleTriggerDto { + /** The number of monthly api calls. */ + readonly limit: number; + /** The number of days to check or null for the current month. */ + readonly numDays?: number | undefined; +} + +export abstract class RuleActionDto implements IRuleActionDto { + /** The discriminator. */ + public readonly actionType!: string; + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + + constructor(data?: IRuleActionDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + (this).actionType = "RuleActionDto"; + } + + init(_data: any) { + this.cleanup(this); + return this; + } + + static fromJSON(data: any): RuleActionDto { + if (data["actionType"] === "Algolia") { + return new AlgoliaRuleActionDto().init(data); + } + if (data["actionType"] === "AzureQueue") { + return new AzureQueueRuleActionDto().init(data); + } + if (data["actionType"] === "Comment") { + return new CommentRuleActionDto().init(data); + } + if (data["actionType"] === "CreateContent") { + return new CreateContentRuleActionDto().init(data); + } + if (data["actionType"] === "Discourse") { + return new DiscourseRuleActionDto().init(data); + } + if (data["actionType"] === "ElasticSearch") { + return new ElasticSearchRuleActionDto().init(data); + } + if (data["actionType"] === "Email") { + return new EmailRuleActionDto().init(data); + } + if (data["actionType"] === "Fastly") { + return new FastlyRuleActionDto().init(data); + } + if (data["actionType"] === "Medium") { + return new MediumRuleActionDto().init(data); + } + if (data["actionType"] === "Notification") { + return new NotificationRuleActionDto().init(data); + } + if (data["actionType"] === "OpenSearch") { + return new OpenSearchRuleActionDto().init(data); + } + if (data["actionType"] === "Prerender") { + return new PrerenderRuleActionDto().init(data); + } + if (data["actionType"] === "Script") { + return new ScriptRuleActionDto().init(data); + } + if (data["actionType"] === "SignalR") { + return new SignalRRuleActionDto().init(data); + } + if (data["actionType"] === "Slack") { + return new SlackRuleActionDto().init(data); + } + if (data["actionType"] === "Tweet") { + return new TweetRuleActionDto().init(data); + } + if (data["actionType"] === "Typesense") { + return new TypesenseRuleActionDto().init(data); + } + if (data["actionType"] === "Webhook") { + return new WebhookRuleActionDto().init(data); + } + throw new Error("The abstract class 'RuleActionDto' cannot be instantiated."); + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["actionType"] = this.actionType; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IRuleActionDto { +} + +export class AlgoliaRuleActionDto extends RuleActionDto implements IAlgoliaRuleActionDto { + /** The application ID. */ + readonly appId!: string; + /** The API key to grant access to Squidex. */ + readonly apiKey!: string; + /** The name of the index. */ + readonly indexName!: string; + /** The optional custom document. */ + readonly document?: string | undefined; + /** The condition when to delete the entry. */ + readonly delete?: string | undefined; + + constructor(data?: IAlgoliaRuleActionDto) { + super(data); + (this).actionType = "Algolia"; + } + + init(_data: any) { + super.init(_data); + (this).appId = _data["appId"]; + (this).apiKey = _data["apiKey"]; + (this).indexName = _data["indexName"]; + (this).document = _data["document"]; + (this).delete = _data["delete"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): AlgoliaRuleActionDto { + const result = new AlgoliaRuleActionDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["appId"] = this.appId; + data["apiKey"] = this.apiKey; + data["indexName"] = this.indexName; + data["document"] = this.document; + data["delete"] = this.delete; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IAlgoliaRuleActionDto extends IRuleActionDto { + /** The application ID. */ + readonly appId: string; + /** The API key to grant access to Squidex. */ + readonly apiKey: string; + /** The name of the index. */ + readonly indexName: string; + /** The optional custom document. */ + readonly document?: string | undefined; + /** The condition when to delete the entry. */ + readonly delete?: string | undefined; +} + +export class AzureQueueRuleActionDto extends RuleActionDto implements IAzureQueueRuleActionDto { + /** The connection string to the storage account. */ + readonly connectionString!: string; + /** The name of the queue. */ + readonly queue!: string; + /** Leave it empty to use the full event as body. */ + readonly payload?: string | undefined; + + constructor(data?: IAzureQueueRuleActionDto) { + super(data); + (this).actionType = "AzureQueue"; + } + + init(_data: any) { + super.init(_data); + (this).connectionString = _data["connectionString"]; + (this).queue = _data["queue"]; + (this).payload = _data["payload"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): AzureQueueRuleActionDto { + const result = new AzureQueueRuleActionDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["connectionString"] = this.connectionString; + data["queue"] = this.queue; + data["payload"] = this.payload; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IAzureQueueRuleActionDto extends IRuleActionDto { + /** The connection string to the storage account. */ + readonly connectionString: string; + /** The name of the queue. */ + readonly queue: string; + /** Leave it empty to use the full event as body. */ + readonly payload?: string | undefined; +} + +export class CommentRuleActionDto extends RuleActionDto implements ICommentRuleActionDto { + /** The comment text. */ + readonly text!: string; + /** An optional client name. */ + readonly client?: string | undefined; + + constructor(data?: ICommentRuleActionDto) { + super(data); + (this).actionType = "Comment"; + } + + init(_data: any) { + super.init(_data); + (this).text = _data["text"]; + (this).client = _data["client"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): CommentRuleActionDto { + const result = new CommentRuleActionDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["text"] = this.text; + data["client"] = this.client; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface ICommentRuleActionDto extends IRuleActionDto { + /** The comment text. */ + readonly text: string; + /** An optional client name. */ + readonly client?: string | undefined; +} + +export class CreateContentRuleActionDto extends RuleActionDto implements ICreateContentRuleActionDto { + /** The content data. */ + readonly data!: string; + /** The name of the schema. */ + readonly schema!: string; + /** An optional client name. */ + readonly client!: string; + /** Publish the content. */ + readonly publish!: boolean; + + constructor(data?: ICreateContentRuleActionDto) { + super(data); + (this).actionType = "CreateContent"; + } + + init(_data: any) { + super.init(_data); + (this).data = _data["data"]; + (this).schema = _data["schema"]; + (this).client = _data["client"]; + (this).publish = _data["publish"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): CreateContentRuleActionDto { + const result = new CreateContentRuleActionDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["data"] = this.data; + data["schema"] = this.schema; + data["client"] = this.client; + data["publish"] = this.publish; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface ICreateContentRuleActionDto extends IRuleActionDto { + /** The content data. */ + readonly data: string; + /** The name of the schema. */ + readonly schema: string; + /** An optional client name. */ + readonly client: string; + /** Publish the content. */ + readonly publish: boolean; +} + +export class DiscourseRuleActionDto extends RuleActionDto implements IDiscourseRuleActionDto { + /** The url to the discourse server. */ + readonly url!: string; + /** The api key to authenticate to your discourse server. */ + readonly apiKey!: string; + /** The api username to authenticate to your discourse server. */ + readonly apiUsername!: string; + /** The text as markdown. */ + readonly text!: string; + /** The optional title when creating new topics. */ + readonly title?: string | undefined; + /** The optional topic id. */ + readonly topic?: number | undefined; + /** The optional category id. */ + readonly category?: number | undefined; + + constructor(data?: IDiscourseRuleActionDto) { + super(data); + (this).actionType = "Discourse"; + } + + init(_data: any) { + super.init(_data); + (this).url = _data["url"]; + (this).apiKey = _data["apiKey"]; + (this).apiUsername = _data["apiUsername"]; + (this).text = _data["text"]; + (this).title = _data["title"]; + (this).topic = _data["topic"]; + (this).category = _data["category"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): DiscourseRuleActionDto { + const result = new DiscourseRuleActionDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["url"] = this.url; + data["apiKey"] = this.apiKey; + data["apiUsername"] = this.apiUsername; + data["text"] = this.text; + data["title"] = this.title; + data["topic"] = this.topic; + data["category"] = this.category; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IDiscourseRuleActionDto extends IRuleActionDto { + /** The url to the discourse server. */ + readonly url: string; + /** The api key to authenticate to your discourse server. */ + readonly apiKey: string; + /** The api username to authenticate to your discourse server. */ + readonly apiUsername: string; + /** The text as markdown. */ + readonly text: string; + /** The optional title when creating new topics. */ + readonly title?: string | undefined; + /** The optional topic id. */ + readonly topic?: number | undefined; + /** The optional category id. */ + readonly category?: number | undefined; +} + +export class ElasticSearchRuleActionDto extends RuleActionDto implements IElasticSearchRuleActionDto { + /** The url to the instance or cluster. */ + readonly host!: string; + /** The name of the index. */ + readonly indexName!: string; + /** The optional username. */ + readonly username?: string | undefined; + /** The optional password. */ + readonly password?: string | undefined; + /** The optional custom document. */ + readonly document?: string | undefined; + /** The condition when to delete the document. */ + readonly delete?: string | undefined; + + constructor(data?: IElasticSearchRuleActionDto) { + super(data); + (this).actionType = "ElasticSearch"; + } + + init(_data: any) { + super.init(_data); + (this).host = _data["host"]; + (this).indexName = _data["indexName"]; + (this).username = _data["username"]; + (this).password = _data["password"]; + (this).document = _data["document"]; + (this).delete = _data["delete"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): ElasticSearchRuleActionDto { + const result = new ElasticSearchRuleActionDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["host"] = this.host; + data["indexName"] = this.indexName; + data["username"] = this.username; + data["password"] = this.password; + data["document"] = this.document; + data["delete"] = this.delete; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IElasticSearchRuleActionDto extends IRuleActionDto { + /** The url to the instance or cluster. */ + readonly host: string; + /** The name of the index. */ + readonly indexName: string; + /** The optional username. */ + readonly username?: string | undefined; + /** The optional password. */ + readonly password?: string | undefined; + /** The optional custom document. */ + readonly document?: string | undefined; + /** The condition when to delete the document. */ + readonly delete?: string | undefined; +} + +export class EmailRuleActionDto extends RuleActionDto implements IEmailRuleActionDto { + /** The IP address or host to the SMTP server. */ + readonly serverHost!: string; + /** The port to the SMTP server. */ + readonly serverPort!: number; + /** The username for the SMTP server. */ + readonly serverUsername!: string; + /** The password for the SMTP server. */ + readonly serverPassword!: string; + /** The email sending address. */ + readonly messageFrom!: string; + /** The email message will be sent to. */ + readonly messageTo!: string; + /** The subject line for this email message. */ + readonly messageSubject!: string; + /** The message body. */ + readonly messageBody!: string; + + constructor(data?: IEmailRuleActionDto) { + super(data); + (this).actionType = "Email"; + } + + init(_data: any) { + super.init(_data); + (this).serverHost = _data["serverHost"]; + (this).serverPort = _data["serverPort"]; + (this).serverUsername = _data["serverUsername"]; + (this).serverPassword = _data["serverPassword"]; + (this).messageFrom = _data["messageFrom"]; + (this).messageTo = _data["messageTo"]; + (this).messageSubject = _data["messageSubject"]; + (this).messageBody = _data["messageBody"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): EmailRuleActionDto { + const result = new EmailRuleActionDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["serverHost"] = this.serverHost; + data["serverPort"] = this.serverPort; + data["serverUsername"] = this.serverUsername; + data["serverPassword"] = this.serverPassword; + data["messageFrom"] = this.messageFrom; + data["messageTo"] = this.messageTo; + data["messageSubject"] = this.messageSubject; + data["messageBody"] = this.messageBody; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IEmailRuleActionDto extends IRuleActionDto { + /** The IP address or host to the SMTP server. */ + readonly serverHost: string; + /** The port to the SMTP server. */ + readonly serverPort: number; + /** The username for the SMTP server. */ + readonly serverUsername: string; + /** The password for the SMTP server. */ + readonly serverPassword: string; + /** The email sending address. */ + readonly messageFrom: string; + /** The email message will be sent to. */ + readonly messageTo: string; + /** The subject line for this email message. */ + readonly messageSubject: string; + /** The message body. */ + readonly messageBody: string; +} + +export class FastlyRuleActionDto extends RuleActionDto implements IFastlyRuleActionDto { + /** The API key to grant access to Squidex. */ + readonly apiKey!: string; + /** The ID of the fastly service. */ + readonly serviceId!: string; + + constructor(data?: IFastlyRuleActionDto) { + super(data); + (this).actionType = "Fastly"; + } + + init(_data: any) { + super.init(_data); + (this).apiKey = _data["apiKey"]; + (this).serviceId = _data["serviceId"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): FastlyRuleActionDto { + const result = new FastlyRuleActionDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["apiKey"] = this.apiKey; + data["serviceId"] = this.serviceId; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IFastlyRuleActionDto extends IRuleActionDto { + /** The API key to grant access to Squidex. */ + readonly apiKey: string; + /** The ID of the fastly service. */ + readonly serviceId: string; +} + +export class MediumRuleActionDto extends RuleActionDto implements IMediumRuleActionDto { + /** The self issued access token. */ + readonly accessToken!: string; + /** The title, used for the url. */ + readonly title!: string; + /** The content, either html or markdown. */ + readonly content!: string; + /** The original home of this content, if it was originally published elsewhere. */ + readonly canonicalUrl?: string | undefined; + /** The optional comma separated list of tags. */ + readonly tags?: string | undefined; + /** Optional publication id. */ + readonly publicationId?: string | undefined; + /** Indicates whether the content is markdown or html. */ + readonly isHtml!: boolean; + + constructor(data?: IMediumRuleActionDto) { + super(data); + (this).actionType = "Medium"; + } + + init(_data: any) { + super.init(_data); + (this).accessToken = _data["accessToken"]; + (this).title = _data["title"]; + (this).content = _data["content"]; + (this).canonicalUrl = _data["canonicalUrl"]; + (this).tags = _data["tags"]; + (this).publicationId = _data["publicationId"]; + (this).isHtml = _data["isHtml"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): MediumRuleActionDto { + const result = new MediumRuleActionDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["accessToken"] = this.accessToken; + data["title"] = this.title; + data["content"] = this.content; + data["canonicalUrl"] = this.canonicalUrl; + data["tags"] = this.tags; + data["publicationId"] = this.publicationId; + data["isHtml"] = this.isHtml; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IMediumRuleActionDto extends IRuleActionDto { + /** The self issued access token. */ + readonly accessToken: string; + /** The title, used for the url. */ + readonly title: string; + /** The content, either html or markdown. */ + readonly content: string; + /** The original home of this content, if it was originally published elsewhere. */ + readonly canonicalUrl?: string | undefined; + /** The optional comma separated list of tags. */ + readonly tags?: string | undefined; + /** Optional publication id. */ + readonly publicationId?: string | undefined; + /** Indicates whether the content is markdown or html. */ + readonly isHtml: boolean; +} + +export class NotificationRuleActionDto extends RuleActionDto implements INotificationRuleActionDto { + /** The user id or email. */ + readonly user!: string; + /** The text to send. */ + readonly text!: string; + /** The optional url to attach to the notification. */ + readonly url?: string | undefined; + /** An optional client name. */ + readonly client?: string | undefined; + + constructor(data?: INotificationRuleActionDto) { + super(data); + (this).actionType = "Notification"; + } + + init(_data: any) { + super.init(_data); + (this).user = _data["user"]; + (this).text = _data["text"]; + (this).url = _data["url"]; + (this).client = _data["client"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): NotificationRuleActionDto { + const result = new NotificationRuleActionDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["user"] = this.user; + data["text"] = this.text; + data["url"] = this.url; + data["client"] = this.client; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface INotificationRuleActionDto extends IRuleActionDto { + /** The user id or email. */ + readonly user: string; + /** The text to send. */ + readonly text: string; + /** The optional url to attach to the notification. */ + readonly url?: string | undefined; + /** An optional client name. */ + readonly client?: string | undefined; +} + +export class OpenSearchRuleActionDto extends RuleActionDto implements IOpenSearchRuleActionDto { + /** The url to the instance or cluster. */ + readonly host!: string; + /** The name of the index. */ + readonly indexName!: string; + /** The optional username. */ + readonly username?: string | undefined; + /** The optional password. */ + readonly password?: string | undefined; + /** The optional custom document. */ + readonly document?: string | undefined; + /** The condition when to delete the document. */ + readonly delete?: string | undefined; + + constructor(data?: IOpenSearchRuleActionDto) { + super(data); + (this).actionType = "OpenSearch"; + } + + init(_data: any) { + super.init(_data); + (this).host = _data["host"]; + (this).indexName = _data["indexName"]; + (this).username = _data["username"]; + (this).password = _data["password"]; + (this).document = _data["document"]; + (this).delete = _data["delete"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): OpenSearchRuleActionDto { + const result = new OpenSearchRuleActionDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["host"] = this.host; + data["indexName"] = this.indexName; + data["username"] = this.username; + data["password"] = this.password; + data["document"] = this.document; + data["delete"] = this.delete; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IOpenSearchRuleActionDto extends IRuleActionDto { + /** The url to the instance or cluster. */ + readonly host: string; + /** The name of the index. */ + readonly indexName: string; + /** The optional username. */ + readonly username?: string | undefined; + /** The optional password. */ + readonly password?: string | undefined; + /** The optional custom document. */ + readonly document?: string | undefined; + /** The condition when to delete the document. */ + readonly delete?: string | undefined; +} + +export class PrerenderRuleActionDto extends RuleActionDto implements IPrerenderRuleActionDto { + /** The prerender token from your account. */ + readonly token!: string; + /** The url to recache. */ + readonly url!: string; + + constructor(data?: IPrerenderRuleActionDto) { + super(data); + (this).actionType = "Prerender"; + } + + init(_data: any) { + super.init(_data); + (this).token = _data["token"]; + (this).url = _data["url"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): PrerenderRuleActionDto { + const result = new PrerenderRuleActionDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["token"] = this.token; + data["url"] = this.url; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IPrerenderRuleActionDto extends IRuleActionDto { + /** The prerender token from your account. */ + readonly token: string; + /** The url to recache. */ + readonly url: string; +} + +export class ScriptRuleActionDto extends RuleActionDto implements IScriptRuleActionDto { + /** The script to render. */ + readonly script!: string; + + constructor(data?: IScriptRuleActionDto) { + super(data); + (this).actionType = "Script"; + } + + init(_data: any) { + super.init(_data); + (this).script = _data["script"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): ScriptRuleActionDto { + const result = new ScriptRuleActionDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["script"] = this.script; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IScriptRuleActionDto extends IRuleActionDto { + /** The script to render. */ + readonly script: string; +} + +export class SignalRRuleActionDto extends RuleActionDto implements ISignalRRuleActionDto { + /** The connection string to the Azure SignalR. */ + readonly connectionString!: string; + /** The name of the hub. */ + readonly hubName!: string; + /** * Broadcast = send to all users. + * User = send to all target users(s). + * Group = send to all target group(s). */ + readonly action!: ActionTypeEnum; + /** Set the Name of the hub method received by the customer. */ + readonly methodName?: string | undefined; + /** Define target users or groups by id or name. One item per line. Not needed for Broadcast action. */ + readonly target?: string | undefined; + /** Leave it empty to use the full event as body. */ + readonly payload?: string | undefined; + + constructor(data?: ISignalRRuleActionDto) { + super(data); + (this).actionType = "SignalR"; + } + + init(_data: any) { + super.init(_data); + (this).connectionString = _data["connectionString"]; + (this).hubName = _data["hubName"]; + (this).action = _data["action"]; + (this).methodName = _data["methodName"]; + (this).target = _data["target"]; + (this).payload = _data["payload"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): SignalRRuleActionDto { + const result = new SignalRRuleActionDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["connectionString"] = this.connectionString; + data["hubName"] = this.hubName; + data["action"] = this.action; + data["methodName"] = this.methodName; + data["target"] = this.target; + data["payload"] = this.payload; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface ISignalRRuleActionDto extends IRuleActionDto { + /** The connection string to the Azure SignalR. */ + readonly connectionString: string; + /** The name of the hub. */ + readonly hubName: string; + /** * Broadcast = send to all users. + * User = send to all target users(s). + * Group = send to all target group(s). */ + readonly action: ActionTypeEnum; + /** Set the Name of the hub method received by the customer. */ + readonly methodName?: string | undefined; + /** Define target users or groups by id or name. One item per line. Not needed for Broadcast action. */ + readonly target?: string | undefined; + /** Leave it empty to use the full event as body. */ + readonly payload?: string | undefined; +} + +export type ActionTypeEnum = "Broadcast" | "User" | "Group"; + +export const ActionTypeEnumValues: ReadonlyArray = [ + "Broadcast", + "User", + "Group" +]; + +export class SlackRuleActionDto extends RuleActionDto implements ISlackRuleActionDto { + /** The slack webhook url. */ + readonly webhookUrl!: string; + /** The text that is sent as message to slack. */ + readonly text!: string; + + constructor(data?: ISlackRuleActionDto) { + super(data); + (this).actionType = "Slack"; + } + + init(_data: any) { + super.init(_data); + (this).webhookUrl = _data["webhookUrl"]; + (this).text = _data["text"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): SlackRuleActionDto { + const result = new SlackRuleActionDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["webhookUrl"] = this.webhookUrl; + data["text"] = this.text; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface ISlackRuleActionDto extends IRuleActionDto { + /** The slack webhook url. */ + readonly webhookUrl: string; + /** The text that is sent as message to slack. */ + readonly text: string; +} + +export class TweetRuleActionDto extends RuleActionDto implements ITweetRuleActionDto { + /** The generated access token. */ + readonly accessToken!: string; + /** The generated access secret. */ + readonly accessSecret!: string; + /** The text that is sent as tweet to twitter. */ + readonly text!: string; + + constructor(data?: ITweetRuleActionDto) { + super(data); + (this).actionType = "Tweet"; + } + + init(_data: any) { + super.init(_data); + (this).accessToken = _data["accessToken"]; + (this).accessSecret = _data["accessSecret"]; + (this).text = _data["text"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): TweetRuleActionDto { + const result = new TweetRuleActionDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["accessToken"] = this.accessToken; + data["accessSecret"] = this.accessSecret; + data["text"] = this.text; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface ITweetRuleActionDto extends IRuleActionDto { + /** The generated access token. */ + readonly accessToken: string; + /** The generated access secret. */ + readonly accessSecret: string; + /** The text that is sent as tweet to twitter. */ + readonly text: string; +} + +export class TypesenseRuleActionDto extends RuleActionDto implements ITypesenseRuleActionDto { + /** The url to the instance or cluster. */ + readonly host!: string; + /** The name of the index. */ + readonly indexName!: string; + /** The api key. */ + readonly apiKey!: string; + /** The optional custom document. */ + readonly document?: string | undefined; + /** The condition when to delete the document. */ + readonly delete?: string | undefined; + + constructor(data?: ITypesenseRuleActionDto) { + super(data); + (this).actionType = "Typesense"; + } + + init(_data: any) { + super.init(_data); + (this).host = _data["host"]; + (this).indexName = _data["indexName"]; + (this).apiKey = _data["apiKey"]; + (this).document = _data["document"]; + (this).delete = _data["delete"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): TypesenseRuleActionDto { + const result = new TypesenseRuleActionDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["host"] = this.host; + data["indexName"] = this.indexName; + data["apiKey"] = this.apiKey; + data["document"] = this.document; + data["delete"] = this.delete; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface ITypesenseRuleActionDto extends IRuleActionDto { + /** The url to the instance or cluster. */ + readonly host: string; + /** The name of the index. */ + readonly indexName: string; + /** The api key. */ + readonly apiKey: string; + /** The optional custom document. */ + readonly document?: string | undefined; + /** The condition when to delete the document. */ + readonly delete?: string | undefined; +} + +export class WebhookRuleActionDto extends RuleActionDto implements IWebhookRuleActionDto { + /** The url to the webhook. */ + readonly url!: string; + /** The type of the request. */ + readonly method!: WebhookMethod; + /** Leave it empty to use the full event as body. */ + readonly payload?: string | undefined; + /** The mime type of the payload. */ + readonly payloadType?: string | undefined; + /** The message headers in the format '[Key]=[Value]', one entry per line. */ + readonly headers?: string | undefined; + /** The shared secret that is used to calculate the payload signature. */ + readonly sharedSecret?: string | undefined; + + constructor(data?: IWebhookRuleActionDto) { + super(data); + (this).actionType = "Webhook"; + } + + init(_data: any) { + super.init(_data); + (this).url = _data["url"]; + (this).method = _data["method"]; + (this).payload = _data["payload"]; + (this).payloadType = _data["payloadType"]; + (this).headers = _data["headers"]; + (this).sharedSecret = _data["sharedSecret"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): WebhookRuleActionDto { + const result = new WebhookRuleActionDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["url"] = this.url; + data["method"] = this.method; + data["payload"] = this.payload; + data["payloadType"] = this.payloadType; + data["headers"] = this.headers; + data["sharedSecret"] = this.sharedSecret; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IWebhookRuleActionDto extends IRuleActionDto { + /** The url to the webhook. */ + readonly url: string; + /** The type of the request. */ + readonly method: WebhookMethod; + /** Leave it empty to use the full event as body. */ + readonly payload?: string | undefined; + /** The mime type of the payload. */ + readonly payloadType?: string | undefined; + /** The message headers in the format '[Key]=[Value]', one entry per line. */ + readonly headers?: string | undefined; + /** The shared secret that is used to calculate the payload signature. */ + readonly sharedSecret?: string | undefined; +} + +export type WebhookMethod = "POST" | "PUT" | "GET" | "DELETE" | "PATCH"; + +export const WebhookMethodValues: ReadonlyArray = [ + "POST", + "PUT", + "GET", + "DELETE", + "PATCH" +]; + +export class CreateRuleDto implements ICreateRuleDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The trigger properties. */ + readonly trigger!: RuleTriggerDto; + /** The action properties. */ + readonly action!: RuleActionDto; + + constructor(data?: ICreateRuleDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).trigger = _data["trigger"] ? RuleTriggerDto.fromJSON(_data["trigger"]) : undefined; + (this).action = _data["action"] ? RuleActionDto.fromJSON(_data["action"]) : undefined; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): CreateRuleDto { + const result = new CreateRuleDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["trigger"] = this.trigger ? this.trigger.toJSON() : undefined; + data["action"] = this.action ? this.action.toJSON() : undefined; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface ICreateRuleDto { + /** The trigger properties. */ + readonly trigger: RuleTriggerDto; + /** The action properties. */ + readonly action: RuleActionDto; +} + +export class UpdateRuleDto implements IUpdateRuleDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** Optional rule name. */ + readonly name?: string | undefined; + /** The trigger properties. */ + readonly trigger?: RuleTriggerDto | undefined; + /** The action properties. */ + readonly action?: RuleActionDto | undefined; + /** Enable or disable the rule. */ + readonly isEnabled?: boolean | undefined; + + constructor(data?: IUpdateRuleDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).name = _data["name"]; + (this).trigger = _data["trigger"] ? RuleTriggerDto.fromJSON(_data["trigger"]) : undefined; + (this).action = _data["action"] ? RuleActionDto.fromJSON(_data["action"]) : undefined; + (this).isEnabled = _data["isEnabled"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): UpdateRuleDto { + const result = new UpdateRuleDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["name"] = this.name; + data["trigger"] = this.trigger ? this.trigger.toJSON() : undefined; + data["action"] = this.action ? this.action.toJSON() : undefined; + data["isEnabled"] = this.isEnabled; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IUpdateRuleDto { + /** Optional rule name. */ + readonly name?: string | undefined; + /** The trigger properties. */ + readonly trigger?: RuleTriggerDto | undefined; + /** The action properties. */ + readonly action?: RuleActionDto | undefined; + /** Enable or disable the rule. */ + readonly isEnabled?: boolean | undefined; +} + +export class SimulatedRuleEventsDto extends ResourceDto implements ISimulatedRuleEventsDto { + /** The total number of simulated rule events. */ + readonly total!: number; + /** The simulated rule events. */ + readonly items!: SimulatedRuleEventDto[]; + + constructor(data?: ISimulatedRuleEventsDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).total = _data["total"]; + if (Array.isArray(_data["items"])) { + (this).items = [] as any; + for (let item of _data["items"]) + (this).items!.push(SimulatedRuleEventDto.fromJSON(item)); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): SimulatedRuleEventsDto { + const result = new SimulatedRuleEventsDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["total"] = this.total; + if (Array.isArray(this.items)) { + data["items"] = []; + for (let item of this.items) + data["items"].push(item.toJSON()); + } + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface ISimulatedRuleEventsDto extends IResourceDto { + /** The total number of simulated rule events. */ + readonly total: number; + /** The simulated rule events. */ + readonly items: SimulatedRuleEventDto[]; +} + +export class SimulatedRuleEventDto implements ISimulatedRuleEventDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The unique event id. */ + readonly eventId!: string; + /** The the unique id of the simulated event. */ + readonly uniqueId!: string; + /** The name of the event. */ + readonly eventName!: string; + /** The source event. */ + readonly event!: any; + /** The enriched event. */ + readonly enrichedEvent?: any | undefined; + /** The data for the action. */ + readonly actionName?: string | undefined; + /** The name of the action. */ + readonly actionData?: string | undefined; + /** The name of the event. */ + readonly error?: string | undefined; + /** The reason why the event has been skipped. */ + readonly skipReasons!: SkipReason[]; + + constructor(data?: ISimulatedRuleEventDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).eventId = _data["eventId"]; + (this).uniqueId = _data["uniqueId"]; + (this).eventName = _data["eventName"]; + (this).event = _data["event"]; + (this).enrichedEvent = _data["enrichedEvent"]; + (this).actionName = _data["actionName"]; + (this).actionData = _data["actionData"]; + (this).error = _data["error"]; + if (Array.isArray(_data["skipReasons"])) { + (this).skipReasons = [] as any; + for (let item of _data["skipReasons"]) + (this).skipReasons!.push(item); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): SimulatedRuleEventDto { + const result = new SimulatedRuleEventDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["eventId"] = this.eventId; + data["uniqueId"] = this.uniqueId; + data["eventName"] = this.eventName; + data["event"] = this.event; + data["enrichedEvent"] = this.enrichedEvent; + data["actionName"] = this.actionName; + data["actionData"] = this.actionData; + data["error"] = this.error; + if (Array.isArray(this.skipReasons)) { + data["skipReasons"] = []; + for (let item of this.skipReasons) + data["skipReasons"].push(item); + } + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface ISimulatedRuleEventDto { + /** The unique event id. */ + readonly eventId: string; + /** The the unique id of the simulated event. */ + readonly uniqueId: string; + /** The name of the event. */ + readonly eventName: string; + /** The source event. */ + readonly event: any; + /** The enriched event. */ + readonly enrichedEvent?: any | undefined; + /** The data for the action. */ + readonly actionName?: string | undefined; + /** The name of the action. */ + readonly actionData?: string | undefined; + /** The name of the event. */ + readonly error?: string | undefined; + /** The reason why the event has been skipped. */ + readonly skipReasons: SkipReason[]; +} + +export type SkipReason = "None" | "ConditionDoesNotMatch" | "ConditionPrecheckDoesNotMatch" | "Disabled" | "Failed" | "FromRule" | "NoAction" | "NoTrigger" | "TooOld" | "WrongEvent" | "WrongEventForTrigger"; + +export const SkipReasonValues: ReadonlyArray = [ + "None", + "ConditionDoesNotMatch", + "ConditionPrecheckDoesNotMatch", + "Disabled", + "Failed", + "FromRule", + "NoAction", + "NoTrigger", + "TooOld", + "WrongEvent", + "WrongEventForTrigger" +]; + +export class RuleEventsDto extends ResourceDto implements IRuleEventsDto { + /** The total number of rule events. */ + readonly total!: number; + /** The rule events. */ + readonly items!: RuleEventDto[]; + + get canCancelAll() { + return this.compute('canCancelAll ', () => hasAnyLink(this._links, 'cancel')); + } + + constructor(data?: IRuleEventsDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).total = _data["total"]; + if (Array.isArray(_data["items"])) { + (this).items = [] as any; + for (let item of _data["items"]) + (this).items!.push(RuleEventDto.fromJSON(item)); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): RuleEventsDto { + const result = new RuleEventsDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["total"] = this.total; + if (Array.isArray(this.items)) { + data["items"] = []; + for (let item of this.items) + data["items"].push(item.toJSON()); + } + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IRuleEventsDto extends IResourceDto { + /** The total number of rule events. */ + readonly total: number; + /** The rule events. */ + readonly items: RuleEventDto[]; +} + +export class RuleEventDto extends ResourceDto implements IRuleEventDto { + /** The ID of the event. */ + readonly id!: string; + /** The time when the event has been created. */ + readonly created!: DateTime; + /** The description. */ + readonly description!: string; + /** The name of the event. */ + readonly eventName!: string; + /** The last dump. */ + readonly lastDump?: string | undefined; + /** The number of calls. */ + readonly numCalls!: number; + /** The next attempt. */ + readonly nextAttempt?: DateTime | undefined; + /** The result of the event. */ + readonly result!: RuleResult; + /** The result of the job. */ + readonly jobResult!: RuleJobResult; + + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'cancel')); + } + + get canUpdate() { + return this.compute('canUpdate', () => hasAnyLink(this._links, 'update')); + } + + constructor(data?: IRuleEventDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).id = _data["id"]; + (this).created = _data["created"] ? DateTime.parseISO(_data["created"].toString()) : undefined; + (this).description = _data["description"]; + (this).eventName = _data["eventName"]; + (this).lastDump = _data["lastDump"]; + (this).numCalls = _data["numCalls"]; + (this).nextAttempt = _data["nextAttempt"] ? DateTime.parseISO(_data["nextAttempt"].toString()) : undefined; + (this).result = _data["result"]; + (this).jobResult = _data["jobResult"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): RuleEventDto { + const result = new RuleEventDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["id"] = this.id; + data["created"] = this.created ? this.created.toISOString() : undefined; + data["description"] = this.description; + data["eventName"] = this.eventName; + data["lastDump"] = this.lastDump; + data["numCalls"] = this.numCalls; + data["nextAttempt"] = this.nextAttempt ? this.nextAttempt.toISOString() : undefined; + data["result"] = this.result; + data["jobResult"] = this.jobResult; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IRuleEventDto extends IResourceDto { + /** The ID of the event. */ + readonly id: string; + /** The time when the event has been created. */ + readonly created: DateTime; + /** The description. */ + readonly description: string; + /** The name of the event. */ + readonly eventName: string; + /** The last dump. */ + readonly lastDump?: string | undefined; + /** The number of calls. */ + readonly numCalls: number; + /** The next attempt. */ + readonly nextAttempt?: DateTime | undefined; + /** The result of the event. */ + readonly result: RuleResult; + /** The result of the job. */ + readonly jobResult: RuleJobResult; +} + +export type RuleResult = "Pending" | "Success" | "Failed" | "Timeout"; + +export const RuleResultValues: ReadonlyArray = [ + "Pending", + "Success", + "Failed", + "Timeout" +]; + +export type RuleJobResult = "Pending" | "Success" | "Retry" | "Failed" | "Cancelled"; + +export const RuleJobResultValues: ReadonlyArray = [ + "Pending", + "Success", + "Retry", + "Failed", + "Cancelled" +]; + +export class PlansDto implements IPlansDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The available plans. */ + readonly plans!: PlanDto[]; + /** The current plan id. */ + readonly currentPlanId?: string | undefined; + /** The plan owner. */ + readonly planOwner?: string | undefined; + /** The link to the management portal. */ + readonly portalLink?: string | undefined; + /** The referral management. */ + readonly referral?: ReferralInfoDto | undefined; + /** The reason why the plan cannot be changed. */ + readonly locked!: PlansLockedReason; + + constructor(data?: IPlansDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + if (Array.isArray(_data["plans"])) { + (this).plans = [] as any; + for (let item of _data["plans"]) + (this).plans!.push(PlanDto.fromJSON(item)); + } + (this).currentPlanId = _data["currentPlanId"]; + (this).planOwner = _data["planOwner"]; + (this).portalLink = _data["portalLink"]; + (this).referral = _data["referral"] ? ReferralInfoDto.fromJSON(_data["referral"]) : undefined; + (this).locked = _data["locked"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): PlansDto { + const result = new PlansDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (Array.isArray(this.plans)) { + data["plans"] = []; + for (let item of this.plans) + data["plans"].push(item.toJSON()); + } + data["currentPlanId"] = this.currentPlanId; + data["planOwner"] = this.planOwner; + data["portalLink"] = this.portalLink; + data["referral"] = this.referral ? this.referral.toJSON() : undefined; + data["locked"] = this.locked; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IPlansDto { + /** The available plans. */ + readonly plans: PlanDto[]; + /** The current plan id. */ + readonly currentPlanId?: string | undefined; + /** The plan owner. */ + readonly planOwner?: string | undefined; + /** The link to the management portal. */ + readonly portalLink?: string | undefined; + /** The referral management. */ + readonly referral?: ReferralInfoDto | undefined; + /** The reason why the plan cannot be changed. */ + readonly locked: PlansLockedReason; +} + +export class PlanDto implements IPlanDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The ID of the plan. */ + readonly id!: string; + /** The name of the plan. */ + readonly name!: string; + /** The monthly costs of the plan. */ + readonly costs!: string; + /** An optional confirm text for the monthly subscription. */ + readonly confirmText?: string | undefined; + /** An optional confirm text for the yearly subscription. */ + readonly yearlyConfirmText?: string | undefined; + /** The yearly costs of the plan. */ + readonly yearlyCosts?: string | undefined; + /** The yearly ID of the plan. */ + readonly yearlyId?: string | undefined; + /** The maximum number of API traffic. */ + readonly maxApiBytes!: number; + /** The maximum number of API calls. */ + readonly maxApiCalls!: number; + /** The maximum allowed asset size. */ + readonly maxAssetSize!: number; + /** The maximum number of contributors. */ + readonly maxContributors!: number; + + constructor(data?: IPlanDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).id = _data["id"]; + (this).name = _data["name"]; + (this).costs = _data["costs"]; + (this).confirmText = _data["confirmText"]; + (this).yearlyConfirmText = _data["yearlyConfirmText"]; + (this).yearlyCosts = _data["yearlyCosts"]; + (this).yearlyId = _data["yearlyId"]; + (this).maxApiBytes = _data["maxApiBytes"]; + (this).maxApiCalls = _data["maxApiCalls"]; + (this).maxAssetSize = _data["maxAssetSize"]; + (this).maxContributors = _data["maxContributors"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): PlanDto { + const result = new PlanDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["id"] = this.id; + data["name"] = this.name; + data["costs"] = this.costs; + data["confirmText"] = this.confirmText; + data["yearlyConfirmText"] = this.yearlyConfirmText; + data["yearlyCosts"] = this.yearlyCosts; + data["yearlyId"] = this.yearlyId; + data["maxApiBytes"] = this.maxApiBytes; + data["maxApiCalls"] = this.maxApiCalls; + data["maxAssetSize"] = this.maxAssetSize; + data["maxContributors"] = this.maxContributors; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IPlanDto { + /** The ID of the plan. */ + readonly id: string; + /** The name of the plan. */ + readonly name: string; + /** The monthly costs of the plan. */ + readonly costs: string; + /** An optional confirm text for the monthly subscription. */ + readonly confirmText?: string | undefined; + /** An optional confirm text for the yearly subscription. */ + readonly yearlyConfirmText?: string | undefined; + /** The yearly costs of the plan. */ + readonly yearlyCosts?: string | undefined; + /** The yearly ID of the plan. */ + readonly yearlyId?: string | undefined; + /** The maximum number of API traffic. */ + readonly maxApiBytes: number; + /** The maximum number of API calls. */ + readonly maxApiCalls: number; + /** The maximum allowed asset size. */ + readonly maxAssetSize: number; + /** The maximum number of contributors. */ + readonly maxContributors: number; +} + +export class ReferralInfoDto implements IReferralInfoDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + readonly code!: string; + readonly earned!: string; + readonly condition!: string; + + constructor(data?: IReferralInfoDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).code = _data["code"]; + (this).earned = _data["earned"]; + (this).condition = _data["condition"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): ReferralInfoDto { + const result = new ReferralInfoDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["code"] = this.code; + data["earned"] = this.earned; + data["condition"] = this.condition; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IReferralInfoDto { + readonly code: string; + readonly earned: string; + readonly condition: string; +} + +export type PlansLockedReason = "None" | "NotOwner" | "NoPermission" | "ManagedByTeam"; + +export const PlansLockedReasonValues: ReadonlyArray = [ + "None", + "NotOwner", + "NoPermission", + "ManagedByTeam" +]; + +export class PlanChangedDto implements IPlanChangedDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** Optional redirect uri. */ + readonly redirectUri?: string | undefined; + + constructor(data?: IPlanChangedDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).redirectUri = _data["redirectUri"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): PlanChangedDto { + const result = new PlanChangedDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["redirectUri"] = this.redirectUri; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IPlanChangedDto { + /** Optional redirect uri. */ + readonly redirectUri?: string | undefined; +} + +export class ChangePlanDto implements IChangePlanDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The new plan id. */ + readonly planId!: string; + + constructor(data?: IChangePlanDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).planId = _data["planId"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): ChangePlanDto { + const result = new ChangePlanDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["planId"] = this.planId; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IChangePlanDto { + /** The new plan id. */ + readonly planId: string; +} + +export class FeaturesDto implements IFeaturesDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The latest features. */ + readonly features!: FeatureDto[]; + /** The recent version. */ + readonly version!: number; + + constructor(data?: IFeaturesDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + if (Array.isArray(_data["features"])) { + (this).features = [] as any; + for (let item of _data["features"]) + (this).features!.push(FeatureDto.fromJSON(item)); + } + (this).version = _data["version"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): FeaturesDto { + const result = new FeaturesDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (Array.isArray(this.features)) { + data["features"] = []; + for (let item of this.features) + data["features"].push(item.toJSON()); + } + data["version"] = this.version; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IFeaturesDto { + /** The latest features. */ + readonly features: FeatureDto[]; + /** The recent version. */ + readonly version: number; +} + +export class FeatureDto implements IFeatureDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The name of the feature. */ + readonly name!: string; + /** The description text. */ + readonly text!: string; + + constructor(data?: IFeatureDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).name = _data["name"]; + (this).text = _data["text"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): FeatureDto { + const result = new FeatureDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["name"] = this.name; + data["text"] = this.text; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IFeatureDto { + /** The name of the feature. */ + readonly name: string; + /** The description text. */ + readonly text: string; +} + +export class LanguageDto implements ILanguageDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The iso code of the language. */ + readonly iso2Code!: string; + /** The english name of the language. */ + readonly englishName!: string; + + constructor(data?: ILanguageDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).iso2Code = _data["iso2Code"]; + (this).englishName = _data["englishName"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): LanguageDto { + const result = new LanguageDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["iso2Code"] = this.iso2Code; + data["englishName"] = this.englishName; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface ILanguageDto { + /** The iso code of the language. */ + readonly iso2Code: string; + /** The english name of the language. */ + readonly englishName: string; +} + +export class JobsDto extends ResourceDto implements IJobsDto { + /** The jobs. */ + readonly items!: JobDto[]; + + get canCreateBackup() { + return this.compute('canCreateBackup', () => hasAnyLink(this._links, 'create/backups')); + } + + constructor(data?: IJobsDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + if (Array.isArray(_data["items"])) { + (this).items = [] as any; + for (let item of _data["items"]) + (this).items!.push(JobDto.fromJSON(item)); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): JobsDto { + const result = new JobsDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (Array.isArray(this.items)) { + data["items"] = []; + for (let item of this.items) + data["items"].push(item.toJSON()); + } + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IJobsDto extends IResourceDto { + /** The jobs. */ + readonly items: JobDto[]; +} + +export class JobDto extends ResourceDto implements IJobDto { + /** The ID of the job. */ + readonly id!: string; + /** The time when the job has been started. */ + readonly started!: DateTime; + /** The time when the job has been stopped. */ + readonly stopped?: DateTime | undefined; + /** The status of the operation. */ + readonly status!: JobStatus; + /** The name of the task. */ + readonly taskName!: string; + /** The description of the job. */ + readonly description!: string; + /** The arguments for the job. */ + readonly taskArguments!: { [key: string]: string; }; + /** The list of log items. */ + readonly log!: JobLogMessageDto[]; + /** Indicates whether the job can be downloaded. */ + readonly canDownload!: boolean; + + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'delete')); + } + + get canDownloadUrl() { + return this.compute('canDownloadUrl', () => hasAnyLink(this._links, 'download')); + } + + get downloadUrl() { + return this.compute('downloadUrl', () => this._links['download']?.href); + } + + get isfailed() { + return this.status === 'Failed'; + } + + constructor(data?: IJobDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).id = _data["id"]; + (this).started = _data["started"] ? DateTime.parseISO(_data["started"].toString()) : undefined; + (this).stopped = _data["stopped"] ? DateTime.parseISO(_data["stopped"].toString()) : undefined; + (this).status = _data["status"]; + (this).taskName = _data["taskName"]; + (this).description = _data["description"]; + if (_data["taskArguments"]) { + (this).taskArguments = {} as any; + for (let key in _data["taskArguments"]) { + if (_data["taskArguments"].hasOwnProperty(key)) + ((this).taskArguments)![key] = _data["taskArguments"][key]; + } + } + if (Array.isArray(_data["log"])) { + (this).log = [] as any; + for (let item of _data["log"]) + (this).log!.push(JobLogMessageDto.fromJSON(item)); + } + (this).canDownload = _data["canDownload"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): JobDto { + const result = new JobDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["id"] = this.id; + data["started"] = this.started ? this.started.toISOString() : undefined; + data["stopped"] = this.stopped ? this.stopped.toISOString() : undefined; + data["status"] = this.status; + data["taskName"] = this.taskName; + data["description"] = this.description; + if (this.taskArguments) { + data["taskArguments"] = {}; + for (let key in this.taskArguments) { + if (this.taskArguments.hasOwnProperty(key)) + (data["taskArguments"])[key] = (this.taskArguments)[key]; + } + } + if (Array.isArray(this.log)) { + data["log"] = []; + for (let item of this.log) + data["log"].push(item.toJSON()); + } + data["canDownload"] = this.canDownload; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IJobDto extends IResourceDto { + /** The ID of the job. */ + readonly id: string; + /** The time when the job has been started. */ + readonly started: DateTime; + /** The time when the job has been stopped. */ + readonly stopped?: DateTime | undefined; + /** The status of the operation. */ + readonly status: JobStatus; + /** The name of the task. */ + readonly taskName: string; + /** The description of the job. */ + readonly description: string; + /** The arguments for the job. */ + readonly taskArguments: { [key: string]: string; }; + /** The list of log items. */ + readonly log: JobLogMessageDto[]; + /** Indicates whether the job can be downloaded. */ + readonly canDownload: boolean; +} + +export type JobStatus = "Created" | "Started" | "Completed" | "Cancelled" | "Failed"; + +export const JobStatusValues: ReadonlyArray = [ + "Created", + "Started", + "Completed", + "Cancelled", + "Failed" +]; + +export class JobLogMessageDto implements IJobLogMessageDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The timestamp. */ + readonly timestamp!: DateTime; + /** The log message. */ + readonly message!: string; + + constructor(data?: IJobLogMessageDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).timestamp = _data["timestamp"] ? DateTime.parseISO(_data["timestamp"].toString()) : undefined; + (this).message = _data["message"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): JobLogMessageDto { + const result = new JobLogMessageDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["timestamp"] = this.timestamp ? this.timestamp.toISOString() : undefined; + data["message"] = this.message; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IJobLogMessageDto { + /** The timestamp. */ + readonly timestamp: DateTime; + /** The log message. */ + readonly message: string; +} + +export class HistoryEventDto implements IHistoryEventDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The message for the event. */ + readonly message!: string; + /** The type of the original event. */ + readonly eventType!: string; + /** The user who called the action. */ + readonly actor!: string; + /** Gets a unique id for the event. */ + readonly eventId!: string; + /** The time when the event happened. */ + readonly created!: DateTime; + /** The version identifier. */ + readonly version!: number; + + constructor(data?: IHistoryEventDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).message = _data["message"]; + (this).eventType = _data["eventType"]; + (this).actor = _data["actor"]; + (this).eventId = _data["eventId"]; + (this).created = _data["created"] ? DateTime.parseISO(_data["created"].toString()) : undefined; + (this).version = _data["version"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): HistoryEventDto { + const result = new HistoryEventDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["message"] = this.message; + data["eventType"] = this.eventType; + data["actor"] = this.actor; + data["eventId"] = this.eventId; + data["created"] = this.created ? this.created.toISOString() : undefined; + data["version"] = this.version; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IHistoryEventDto { + /** The message for the event. */ + readonly message: string; + /** The type of the original event. */ + readonly eventType: string; + /** The user who called the action. */ + readonly actor: string; + /** Gets a unique id for the event. */ + readonly eventId: string; + /** The time when the event happened. */ + readonly created: DateTime; + /** The version identifier. */ + readonly version: number; +} + +export class EventConsumersDto extends ResourceDto implements IEventConsumersDto { + /** The event consumers. */ + readonly items!: EventConsumerDto[]; + + constructor(data?: IEventConsumersDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + if (Array.isArray(_data["items"])) { + (this).items = [] as any; + for (let item of _data["items"]) + (this).items!.push(EventConsumerDto.fromJSON(item)); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): EventConsumersDto { + const result = new EventConsumersDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (Array.isArray(this.items)) { + data["items"] = []; + for (let item of this.items) + data["items"].push(item.toJSON()); + } + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IEventConsumersDto extends IResourceDto { + /** The event consumers. */ + readonly items: EventConsumerDto[]; +} + +export class EventConsumerDto extends ResourceDto implements IEventConsumerDto { + /** Indicates if the event consumer has been started. */ + readonly isStopped!: boolean; + /** Indicates if the event consumer is resetting at the moment. */ + readonly isResetting!: boolean; + /** The number of handled events. */ + readonly count!: number; + /** The name of the event consumer. */ + readonly name!: string; + /** The error details if the event consumer has been stopped after a failure. */ + readonly error?: string | undefined; + /** The position within the vent stream. */ + readonly position?: string | undefined; + + get canReset() { + return this.compute('reset', () => hasAnyLink(this._links, 'reset')); + } + + get canStart() { + return this.compute('canStart', () => hasAnyLink(this._links, 'start')); + } + + get canStop() { + return this.compute('canStop', () => hasAnyLink(this._links, 'stop')); + } + + constructor(data?: IEventConsumerDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).isStopped = _data["isStopped"]; + (this).isResetting = _data["isResetting"]; + (this).count = _data["count"]; + (this).name = _data["name"]; + (this).error = _data["error"]; + (this).position = _data["position"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): EventConsumerDto { + const result = new EventConsumerDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["isStopped"] = this.isStopped; + data["isResetting"] = this.isResetting; + data["count"] = this.count; + data["name"] = this.name; + data["error"] = this.error; + data["position"] = this.position; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IEventConsumerDto extends IResourceDto { + /** Indicates if the event consumer has been started. */ + readonly isStopped: boolean; + /** Indicates if the event consumer is resetting at the moment. */ + readonly isResetting: boolean; + /** The number of handled events. */ + readonly count: number; + /** The name of the event consumer. */ + readonly name: string; + /** The error details if the event consumer has been stopped after a failure. */ + readonly error?: string | undefined; + /** The position within the vent stream. */ + readonly position?: string | undefined; +} + +export class ContentsDto extends ResourceDto implements IContentsDto { + /** The total number of content items. */ + readonly total!: number; + /** The content items. */ + readonly items!: ContentDto[]; + /** The possible statuses. */ + readonly statuses!: StatusInfoDto[]; + + get canCreate() { + return this.compute('canCreate', () => hasAnyLink(this._links, 'create')); + } + + get canCreateAndPublish() { + return this.compute('canCreate', () => hasAnyLink(this._links, 'create/publish')); + } + + constructor(data?: IContentsDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).total = _data["total"]; + if (Array.isArray(_data["items"])) { + (this).items = [] as any; + for (let item of _data["items"]) + (this).items!.push(ContentDto.fromJSON(item)); + } + if (Array.isArray(_data["statuses"])) { + (this).statuses = [] as any; + for (let item of _data["statuses"]) + (this).statuses!.push(StatusInfoDto.fromJSON(item)); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): ContentsDto { + const result = new ContentsDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["total"] = this.total; + if (Array.isArray(this.items)) { + data["items"] = []; + for (let item of this.items) + data["items"].push(item.toJSON()); + } + if (Array.isArray(this.statuses)) { + data["statuses"] = []; + for (let item of this.statuses) + data["statuses"].push(item.toJSON()); + } + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IContentsDto extends IResourceDto { + /** The total number of content items. */ + readonly total: number; + /** The content items. */ + readonly items: ContentDto[]; + /** The possible statuses. */ + readonly statuses: StatusInfoDto[]; +} + +export class ContentDto extends ResourceDto implements IContentDto { + /** The if of the content item. */ + readonly id!: string; + /** The user that has created the content item. */ + readonly createdBy!: string; + /** The user that has updated the content item. */ + readonly lastModifiedBy!: string; + /** The data of the content item. */ + readonly data!: any; + /** The reference data for the frontend UI. */ + readonly referenceData?: { [key: string]: { [key: string]: any; }; } | undefined; + /** The date and time when the content item has been created. */ + readonly created!: DateTime; + /** The date and time when the content item has been modified last. */ + readonly lastModified!: DateTime; + /** The status of the content. */ + readonly status!: string; + /** The new status of the content. */ + readonly newStatus?: string | undefined; + /** The color of the status. */ + readonly statusColor!: string; + /** The color of the new status. */ + readonly newStatusColor?: string | undefined; + /** The UI token. */ + readonly editToken?: string | undefined; + /** The scheduled status. */ + readonly scheduleJob?: ScheduleJobDto | undefined; + /** The ID of the schema. */ + readonly schemaId!: string; + /** The name of the schema. */ + readonly schemaName!: string; + /** The display name of the schema. */ + readonly schemaDisplayName!: string; + /** The reference fields. */ + readonly referenceFields!: FieldDto[]; + /** Indicates whether the content is deleted. */ + readonly isDeleted!: boolean; + /** The version of the content. */ + readonly version!: number; + + get canPublish() { + return this.compute('canPublish', () => this.statusUpdates.find(x => x.status === 'Published')); + } + + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'update')); + } + + get canDraftCreate() { + return this.compute('canDraftCreate', () => hasAnyLink(this._links, 'draft/create')); + } + + get canDraftDelete() { + return this.compute('canDraftDelete', () => hasAnyLink(this._links, 'draft/delete')); + } + + get canCancelStatus() { + return this.compute('canCancelStatus', () => hasAnyLink(this._links, 'cancel')); + } + + get canUpdate() { + return this.compute('canUpdate', () => hasAnyLink(this._links, 'update')); + } + + get statusUpdates() { + return this.compute('statusUpdates', () => { + const updates: { status: string; color: string }[] = []; + for (const [key, link] of Object.entries(this._links)) { + if (key.startsWith('status/')) { + updates.push({ status: key.substring(7), color: link.metadata! }); + } + } + + return updates; + }); + } + + constructor(data?: IContentDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).id = _data["id"]; + (this).createdBy = _data["createdBy"]; + (this).lastModifiedBy = _data["lastModifiedBy"]; + (this).data = _data["data"]; + if (_data["referenceData"]) { + (this).referenceData = {} as any; + for (let key in _data["referenceData"]) { + if (_data["referenceData"].hasOwnProperty(key)) + ((this).referenceData)![key] = _data["referenceData"][key]; + } + } + (this).created = _data["created"] ? DateTime.parseISO(_data["created"].toString()) : undefined; + (this).lastModified = _data["lastModified"] ? DateTime.parseISO(_data["lastModified"].toString()) : undefined; + (this).status = _data["status"]; + (this).newStatus = _data["newStatus"]; + (this).statusColor = _data["statusColor"]; + (this).newStatusColor = _data["newStatusColor"]; + (this).editToken = _data["editToken"]; + (this).scheduleJob = _data["scheduleJob"] ? ScheduleJobDto.fromJSON(_data["scheduleJob"]) : undefined; + (this).schemaId = _data["schemaId"]; + (this).schemaName = _data["schemaName"]; + (this).schemaDisplayName = _data["schemaDisplayName"]; + if (Array.isArray(_data["referenceFields"])) { + (this).referenceFields = [] as any; + for (let item of _data["referenceFields"]) + (this).referenceFields!.push(FieldDto.fromJSON(item)); + } + (this).isDeleted = _data["isDeleted"]; + (this).version = _data["version"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): ContentDto { + const result = new ContentDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["id"] = this.id; + data["createdBy"] = this.createdBy; + data["lastModifiedBy"] = this.lastModifiedBy; + data["data"] = this.data; + if (this.referenceData) { + data["referenceData"] = {}; + for (let key in this.referenceData) { + if (this.referenceData.hasOwnProperty(key)) + (data["referenceData"])[key] = (this.referenceData)[key]; + } + } + data["created"] = this.created ? this.created.toISOString() : undefined; + data["lastModified"] = this.lastModified ? this.lastModified.toISOString() : undefined; + data["status"] = this.status; + data["newStatus"] = this.newStatus; + data["statusColor"] = this.statusColor; + data["newStatusColor"] = this.newStatusColor; + data["editToken"] = this.editToken; + data["scheduleJob"] = this.scheduleJob ? this.scheduleJob.toJSON() : undefined; + data["schemaId"] = this.schemaId; + data["schemaName"] = this.schemaName; + data["schemaDisplayName"] = this.schemaDisplayName; + if (Array.isArray(this.referenceFields)) { + data["referenceFields"] = []; + for (let item of this.referenceFields) + data["referenceFields"].push(item.toJSON()); + } + data["isDeleted"] = this.isDeleted; + data["version"] = this.version; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IContentDto extends IResourceDto { + /** The if of the content item. */ + readonly id: string; + /** The user that has created the content item. */ + readonly createdBy: string; + /** The user that has updated the content item. */ + readonly lastModifiedBy: string; + /** The data of the content item. */ + readonly data: any; + /** The reference data for the frontend UI. */ + readonly referenceData?: { [key: string]: { [key: string]: any; }; } | undefined; + /** The date and time when the content item has been created. */ + readonly created: DateTime; + /** The date and time when the content item has been modified last. */ + readonly lastModified: DateTime; + /** The status of the content. */ + readonly status: string; + /** The new status of the content. */ + readonly newStatus?: string | undefined; + /** The color of the status. */ + readonly statusColor: string; + /** The color of the new status. */ + readonly newStatusColor?: string | undefined; + /** The UI token. */ + readonly editToken?: string | undefined; + /** The scheduled status. */ + readonly scheduleJob?: ScheduleJobDto | undefined; + /** The ID of the schema. */ + readonly schemaId: string; + /** The name of the schema. */ + readonly schemaName: string; + /** The display name of the schema. */ + readonly schemaDisplayName: string; + /** The reference fields. */ + readonly referenceFields: FieldDto[]; + /** Indicates whether the content is deleted. */ + readonly isDeleted: boolean; + /** The version of the content. */ + readonly version: number; +} + +export class ScheduleJobDto implements IScheduleJobDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The ID of the schedule job. */ + readonly id!: string; + /** The new status. */ + readonly status!: string; + /** The target date and time when the content should be scheduled. */ + readonly dueTime!: DateTime; + /** The color of the scheduled status. */ + readonly color!: string; + /** The user who schedule the content. */ + readonly scheduledBy!: string; + + constructor(data?: IScheduleJobDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).id = _data["id"]; + (this).status = _data["status"]; + (this).dueTime = _data["dueTime"] ? DateTime.parseISO(_data["dueTime"].toString()) : undefined; + (this).color = _data["color"]; + (this).scheduledBy = _data["scheduledBy"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): ScheduleJobDto { + const result = new ScheduleJobDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["id"] = this.id; + data["status"] = this.status; + data["dueTime"] = this.dueTime ? this.dueTime.toISOString() : undefined; + data["color"] = this.color; + data["scheduledBy"] = this.scheduledBy; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IScheduleJobDto { + /** The ID of the schedule job. */ + readonly id: string; + /** The new status. */ + readonly status: string; + /** The target date and time when the content should be scheduled. */ + readonly dueTime: DateTime; + /** The color of the scheduled status. */ + readonly color: string; + /** The user who schedule the content. */ + readonly scheduledBy: string; +} + +export class StatusInfoDto implements IStatusInfoDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The name of the status. */ + readonly status!: string; + /** The color of the status. */ + readonly color!: string; + + constructor(data?: IStatusInfoDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).status = _data["status"]; + (this).color = _data["color"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): StatusInfoDto { + const result = new StatusInfoDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["status"] = this.status; + data["color"] = this.color; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IStatusInfoDto { + /** The name of the status. */ + readonly status: string; + /** The color of the status. */ + readonly color: string; +} + +export class QueryDto implements IQueryDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The optional list of ids to query. */ + readonly ids?: string[] | undefined; + /** The optional odata query. */ + readonly oData?: string | undefined; + /** The optional json query. */ + readonly q?: any | undefined; + /** The parent id (for assets). */ + readonly parentId?: string | undefined; + + constructor(data?: IQueryDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + if (Array.isArray(_data["ids"])) { + (this).ids = [] as any; + for (let item of _data["ids"]) + (this).ids!.push(item); + } + (this).oData = _data["oData"]; + (this).q = _data["q"]; + (this).parentId = _data["parentId"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): QueryDto { + const result = new QueryDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (Array.isArray(this.ids)) { + data["ids"] = []; + for (let item of this.ids) + data["ids"].push(item); + } + data["oData"] = this.oData; + data["q"] = this.q; + data["parentId"] = this.parentId; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IQueryDto { + /** The optional list of ids to query. */ + readonly ids?: string[] | undefined; + /** The optional odata query. */ + readonly oData?: string | undefined; + /** The optional json query. */ + readonly q?: any | undefined; + /** The parent id (for assets). */ + readonly parentId?: string | undefined; +} + +export class BulkResultDto implements IBulkResultDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The error when the bulk job failed. */ + readonly error?: ServerErrorDto | undefined; + /** The index of the bulk job where the result belongs to. The order can change. */ + readonly jobIndex!: number; + /** The ID of the entity that has been handled successfully or not. */ + readonly id?: string | undefined; + /** The ID of the entity that has been handled successfully or not. */ + readonly contentId?: string | undefined; + + constructor(data?: IBulkResultDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).error = _data["error"] ? ServerErrorDto.fromJSON(_data["error"]) : undefined; + (this).jobIndex = _data["jobIndex"]; + (this).id = _data["id"]; + (this).contentId = _data["contentId"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): BulkResultDto { + const result = new BulkResultDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["error"] = this.error ? this.error.toJSON() : undefined; + data["jobIndex"] = this.jobIndex; + data["id"] = this.id; + data["contentId"] = this.contentId; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IBulkResultDto { + /** The error when the bulk job failed. */ + readonly error?: ServerErrorDto | undefined; + /** The index of the bulk job where the result belongs to. The order can change. */ + readonly jobIndex: number; + /** The ID of the entity that has been handled successfully or not. */ + readonly id?: string | undefined; + /** The ID of the entity that has been handled successfully or not. */ + readonly contentId?: string | undefined; +} + +export class ImportContentsDto implements IImportContentsDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The data to import. */ + readonly datas!: { [key: string]: { [key: string]: any; }; }[]; + /** True to automatically publish the content. */ + readonly publish?: boolean; + /** True to turn off scripting for faster inserts. Default: true. */ + readonly doNotScript?: boolean; + /** True to turn off costly validation: Unique checks, asset checks and reference checks. Default: true. */ + readonly optimizeValidation?: boolean; + + constructor(data?: IImportContentsDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + if (Array.isArray(_data["datas"])) { + (this).datas = [] as any; + for (let item of _data["datas"]) + (this).datas!.push(item); + } + (this).publish = _data["publish"]; + (this).doNotScript = _data["doNotScript"]; + (this).optimizeValidation = _data["optimizeValidation"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): ImportContentsDto { + const result = new ImportContentsDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (Array.isArray(this.datas)) { + data["datas"] = []; + for (let item of this.datas) + data["datas"].push(item); + } + data["publish"] = this.publish; + data["doNotScript"] = this.doNotScript; + data["optimizeValidation"] = this.optimizeValidation; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IImportContentsDto { + /** The data to import. */ + readonly datas: { [key: string]: { [key: string]: any; }; }[]; + /** True to automatically publish the content. */ + readonly publish?: boolean; + /** True to turn off scripting for faster inserts. Default: true. */ + readonly doNotScript?: boolean; + /** True to turn off costly validation: Unique checks, asset checks and reference checks. Default: true. */ + readonly optimizeValidation?: boolean; +} + +export class BulkUpdateContentsDto implements IBulkUpdateContentsDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The contents to update or insert. */ + readonly jobs!: BulkUpdateContentsJobDto[]; + /** True to automatically publish the content. */ + readonly publish?: boolean; + /** True to turn off scripting for faster inserts. Default: true. */ + readonly doNotScript?: boolean; + /** True, to also enrich required fields. Default: false. */ + readonly enrichRequiredFields?: boolean; + /** True to turn off validation for faster inserts. Default: false. */ + readonly doNotValidate?: boolean; + /** True to turn off validation of workflow rules. Default: false. */ + readonly doNotValidateWorkflow?: boolean; + /** True to check referrers of deleted contents. */ + readonly checkReferrers?: boolean; + /** True to turn off costly validation: Unique checks, asset checks and reference checks. Default: true. */ + readonly optimizeValidation?: boolean; + + constructor(data?: IBulkUpdateContentsDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + if (Array.isArray(_data["jobs"])) { + (this).jobs = [] as any; + for (let item of _data["jobs"]) + (this).jobs!.push(BulkUpdateContentsJobDto.fromJSON(item)); + } + (this).publish = _data["publish"]; + (this).doNotScript = _data["doNotScript"]; + (this).enrichRequiredFields = _data["enrichRequiredFields"]; + (this).doNotValidate = _data["doNotValidate"]; + (this).doNotValidateWorkflow = _data["doNotValidateWorkflow"]; + (this).checkReferrers = _data["checkReferrers"]; + (this).optimizeValidation = _data["optimizeValidation"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): BulkUpdateContentsDto { + const result = new BulkUpdateContentsDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (Array.isArray(this.jobs)) { + data["jobs"] = []; + for (let item of this.jobs) + data["jobs"].push(item.toJSON()); + } + data["publish"] = this.publish; + data["doNotScript"] = this.doNotScript; + data["enrichRequiredFields"] = this.enrichRequiredFields; + data["doNotValidate"] = this.doNotValidate; + data["doNotValidateWorkflow"] = this.doNotValidateWorkflow; + data["checkReferrers"] = this.checkReferrers; + data["optimizeValidation"] = this.optimizeValidation; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IBulkUpdateContentsDto { + /** The contents to update or insert. */ + readonly jobs: BulkUpdateContentsJobDto[]; + /** True to automatically publish the content. */ + readonly publish?: boolean; + /** True to turn off scripting for faster inserts. Default: true. */ + readonly doNotScript?: boolean; + /** True, to also enrich required fields. Default: false. */ + readonly enrichRequiredFields?: boolean; + /** True to turn off validation for faster inserts. Default: false. */ + readonly doNotValidate?: boolean; + /** True to turn off validation of workflow rules. Default: false. */ + readonly doNotValidateWorkflow?: boolean; + /** True to check referrers of deleted contents. */ + readonly checkReferrers?: boolean; + /** True to turn off costly validation: Unique checks, asset checks and reference checks. Default: true. */ + readonly optimizeValidation?: boolean; +} + +export class BulkUpdateContentsJobDto implements IBulkUpdateContentsJobDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** An optional query to identify the content to update. */ + readonly query?: QueryJsonDto | undefined; + /** An optional ID of the content to update. */ + readonly id?: string | undefined; + /** The data of the content when type is set to 'Upsert', 'Create', 'Update' or 'Patch. */ + readonly data?: { [key: string]: { [key: string]: any; }; } | undefined; + /** The new status when the type is set to 'ChangeStatus' or 'Upsert'. */ + readonly status?: string | undefined; + /** The due time. */ + readonly dueTime?: DateTime | undefined; + /** The update type. */ + readonly type?: BulkUpdateContentType; + /** The optional schema id or name. */ + readonly schema?: string | undefined; + /** Makes the update as patch. */ + readonly patch?: boolean; + /** True to delete the content permanently. */ + readonly permanent?: boolean; + /** Enrich the data with the default values when updating a content item. */ + readonly enrichDefaults?: boolean; + /** The number of expected items. Set it to a higher number to update multiple items when a query is defined. */ + readonly expectedCount?: number; + /** The expected version. */ + readonly expectedVersion?: number; + + constructor(data?: IBulkUpdateContentsJobDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).query = _data["query"] ? QueryJsonDto.fromJSON(_data["query"]) : undefined; + (this).id = _data["id"]; + if (_data["data"]) { + (this).data = {} as any; + for (let key in _data["data"]) { + if (_data["data"].hasOwnProperty(key)) + ((this).data)![key] = _data["data"][key]; + } + } + (this).status = _data["status"]; + (this).dueTime = _data["dueTime"] ? DateTime.parseISO(_data["dueTime"].toString()) : undefined; + (this).type = _data["type"]; + (this).schema = _data["schema"]; + (this).patch = _data["patch"]; + (this).permanent = _data["permanent"]; + (this).enrichDefaults = _data["enrichDefaults"]; + (this).expectedCount = _data["expectedCount"]; + (this).expectedVersion = _data["expectedVersion"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): BulkUpdateContentsJobDto { + const result = new BulkUpdateContentsJobDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["query"] = this.query ? this.query.toJSON() : undefined; + data["id"] = this.id; + if (this.data) { + data["data"] = {}; + for (let key in this.data) { + if (this.data.hasOwnProperty(key)) + (data["data"])[key] = (this.data)[key]; + } + } + data["status"] = this.status; + data["dueTime"] = this.dueTime ? this.dueTime.toISOString() : undefined; + data["type"] = this.type; + data["schema"] = this.schema; + data["patch"] = this.patch; + data["permanent"] = this.permanent; + data["enrichDefaults"] = this.enrichDefaults; + data["expectedCount"] = this.expectedCount; + data["expectedVersion"] = this.expectedVersion; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IBulkUpdateContentsJobDto { + /** An optional query to identify the content to update. */ + readonly query?: QueryJsonDto | undefined; + /** An optional ID of the content to update. */ + readonly id?: string | undefined; + /** The data of the content when type is set to 'Upsert', 'Create', 'Update' or 'Patch. */ + readonly data?: { [key: string]: { [key: string]: any; }; } | undefined; + /** The new status when the type is set to 'ChangeStatus' or 'Upsert'. */ + readonly status?: string | undefined; + /** The due time. */ + readonly dueTime?: DateTime | undefined; + /** The update type. */ + readonly type?: BulkUpdateContentType; + /** The optional schema id or name. */ + readonly schema?: string | undefined; + /** Makes the update as patch. */ + readonly patch?: boolean; + /** True to delete the content permanently. */ + readonly permanent?: boolean; + /** Enrich the data with the default values when updating a content item. */ + readonly enrichDefaults?: boolean; + /** The number of expected items. Set it to a higher number to update multiple items when a query is defined. */ + readonly expectedCount?: number; + /** The expected version. */ + readonly expectedVersion?: number; +} + +export class QueryJsonDto implements IQueryJsonDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + readonly filter?: any | undefined; + readonly fullText?: string | undefined; + readonly collation?: string | undefined; + readonly skip!: number; + readonly take!: number; + readonly random!: number; + readonly top!: number; + readonly sort?: SortNodeDto[] | undefined; + + constructor(data?: IQueryJsonDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).filter = _data["filter"]; + (this).fullText = _data["fullText"]; + (this).collation = _data["collation"]; + (this).skip = _data["skip"]; + (this).take = _data["take"]; + (this).random = _data["random"]; + (this).top = _data["top"]; + if (Array.isArray(_data["sort"])) { + (this).sort = [] as any; + for (let item of _data["sort"]) + (this).sort!.push(SortNodeDto.fromJSON(item)); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): QueryJsonDto { + const result = new QueryJsonDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["filter"] = this.filter; + data["fullText"] = this.fullText; + data["collation"] = this.collation; + data["skip"] = this.skip; + data["take"] = this.take; + data["random"] = this.random; + data["top"] = this.top; + if (Array.isArray(this.sort)) { + data["sort"] = []; + for (let item of this.sort) + data["sort"].push(item.toJSON()); + } + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IQueryJsonDto { + readonly filter?: any | undefined; + readonly fullText?: string | undefined; + readonly collation?: string | undefined; + readonly skip: number; + readonly take: number; + readonly random: number; + readonly top: number; + readonly sort?: SortNodeDto[] | undefined; +} + +export class SortNodeDto implements ISortNodeDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + readonly path!: string; + readonly order!: SortOrder; + + constructor(data?: ISortNodeDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).path = _data["path"]; + (this).order = _data["order"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): SortNodeDto { + const result = new SortNodeDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["path"] = this.path; + data["order"] = this.order; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface ISortNodeDto { + readonly path: string; + readonly order: SortOrder; +} + +export type BulkUpdateContentType = "Upsert" | "ChangeStatus" | "Create" | "Delete" | "Patch" | "Update" | "Validate" | "EnrichDefaults"; + +export const BulkUpdateContentTypeValues: ReadonlyArray = [ + "Upsert", + "ChangeStatus", + "Create", + "Delete", + "Patch", + "Update", + "Validate", + "EnrichDefaults" +]; + +export class ChangeStatusDto implements IChangeStatusDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The new status. */ + readonly status!: string; + /** The due time. */ + readonly dueTime?: DateTime | undefined; + /** True to check referrers of this content. */ + readonly checkReferrers?: boolean; + + constructor(data?: IChangeStatusDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).status = _data["status"]; + (this).dueTime = _data["dueTime"] ? DateTime.parseISO(_data["dueTime"].toString()) : undefined; + (this).checkReferrers = _data["checkReferrers"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): ChangeStatusDto { + const result = new ChangeStatusDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["status"] = this.status; + data["dueTime"] = this.dueTime ? this.dueTime.toISOString() : undefined; + data["checkReferrers"] = this.checkReferrers; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IChangeStatusDto { + /** The new status. */ + readonly status: string; + /** The due time. */ + readonly dueTime?: DateTime | undefined; + /** True to check referrers of this content. */ + readonly checkReferrers?: boolean; +} + +export class AllContentsByPostDto implements IAllContentsByPostDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The list of ids to query. */ + readonly ids?: string[] | undefined; + /** The start of the schedule. */ + readonly scheduledFrom?: DateTime | undefined; + /** The end of the schedule. */ + readonly scheduledTo?: DateTime | undefined; + /** The ID of the referencing content item. */ + readonly referencing?: string | undefined; + /** The ID of the reference content item. */ + readonly references?: string | undefined; + /** The optional odata query. */ + readonly oData?: string | undefined; + /** The optional json query. */ + readonly q?: any | undefined; + + constructor(data?: IAllContentsByPostDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + if (Array.isArray(_data["ids"])) { + (this).ids = [] as any; + for (let item of _data["ids"]) + (this).ids!.push(item); + } + (this).scheduledFrom = _data["scheduledFrom"] ? DateTime.parseISO(_data["scheduledFrom"].toString()) : undefined; + (this).scheduledTo = _data["scheduledTo"] ? DateTime.parseISO(_data["scheduledTo"].toString()) : undefined; + (this).referencing = _data["referencing"]; + (this).references = _data["references"]; + (this).oData = _data["oData"]; + (this).q = _data["q"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): AllContentsByPostDto { + const result = new AllContentsByPostDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (Array.isArray(this.ids)) { + data["ids"] = []; + for (let item of this.ids) + data["ids"].push(item); + } + data["scheduledFrom"] = this.scheduledFrom ? this.scheduledFrom.toISOString() : undefined; + data["scheduledTo"] = this.scheduledTo ? this.scheduledTo.toISOString() : undefined; + data["referencing"] = this.referencing; + data["references"] = this.references; + data["oData"] = this.oData; + data["q"] = this.q; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IAllContentsByPostDto { + /** The list of ids to query. */ + readonly ids?: string[] | undefined; + /** The start of the schedule. */ + readonly scheduledFrom?: DateTime | undefined; + /** The end of the schedule. */ + readonly scheduledTo?: DateTime | undefined; + /** The ID of the referencing content item. */ + readonly referencing?: string | undefined; + /** The ID of the reference content item. */ + readonly references?: string | undefined; + /** The optional odata query. */ + readonly oData?: string | undefined; + /** The optional json query. */ + readonly q?: any | undefined; +} + +export class BackupJobsDto extends ResourceDto implements IBackupJobsDto { + /** The backups. */ + readonly items!: BackupJobDto[]; + + constructor(data?: IBackupJobsDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + if (Array.isArray(_data["items"])) { + (this).items = [] as any; + for (let item of _data["items"]) + (this).items!.push(BackupJobDto.fromJSON(item)); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): BackupJobsDto { + const result = new BackupJobsDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (Array.isArray(this.items)) { + data["items"] = []; + for (let item of this.items) + data["items"].push(item.toJSON()); + } + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IBackupJobsDto extends IResourceDto { + /** The backups. */ + readonly items: BackupJobDto[]; +} + +export class BackupJobDto extends ResourceDto implements IBackupJobDto { + /** The ID of the backup job. */ + readonly id!: string; + /** The time when the job has been started. */ + readonly started!: DateTime; + /** The time when the job has been stopped. */ + readonly stopped?: DateTime | undefined; + /** The number of handled events. */ + readonly handledEvents!: number; + /** The number of handled assets. */ + readonly handledAssets!: number; + /** The status of the operation. */ + readonly status!: JobStatus; + + constructor(data?: IBackupJobDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).id = _data["id"]; + (this).started = _data["started"] ? DateTime.parseISO(_data["started"].toString()) : undefined; + (this).stopped = _data["stopped"] ? DateTime.parseISO(_data["stopped"].toString()) : undefined; + (this).handledEvents = _data["handledEvents"]; + (this).handledAssets = _data["handledAssets"]; + (this).status = _data["status"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): BackupJobDto { + const result = new BackupJobDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["id"] = this.id; + data["started"] = this.started ? this.started.toISOString() : undefined; + data["stopped"] = this.stopped ? this.stopped.toISOString() : undefined; + data["handledEvents"] = this.handledEvents; + data["handledAssets"] = this.handledAssets; + data["status"] = this.status; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IBackupJobDto extends IResourceDto { + /** The ID of the backup job. */ + readonly id: string; + /** The time when the job has been started. */ + readonly started: DateTime; + /** The time when the job has been stopped. */ + readonly stopped?: DateTime | undefined; + /** The number of handled events. */ + readonly handledEvents: number; + /** The number of handled assets. */ + readonly handledAssets: number; + /** The status of the operation. */ + readonly status: JobStatus; +} + +export class RestoreJobDto implements IRestoreJobDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The uri to load from. */ + readonly url!: string; + /** The status log. */ + readonly log!: string[]; + /** The time when the job has been started. */ + readonly started!: DateTime; + /** The time when the job has been stopped. */ + readonly stopped?: DateTime | undefined; + /** The status of the operation. */ + readonly status!: JobStatus; + + constructor(data?: IRestoreJobDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).url = _data["url"]; + if (Array.isArray(_data["log"])) { + (this).log = [] as any; + for (let item of _data["log"]) + (this).log!.push(item); + } + (this).started = _data["started"] ? DateTime.parseISO(_data["started"].toString()) : undefined; + (this).stopped = _data["stopped"] ? DateTime.parseISO(_data["stopped"].toString()) : undefined; + (this).status = _data["status"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): RestoreJobDto { + const result = new RestoreJobDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["url"] = this.url; + if (Array.isArray(this.log)) { + data["log"] = []; + for (let item of this.log) + data["log"].push(item); + } + data["started"] = this.started ? this.started.toISOString() : undefined; + data["stopped"] = this.stopped ? this.stopped.toISOString() : undefined; + data["status"] = this.status; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IRestoreJobDto { + /** The uri to load from. */ + readonly url: string; + /** The status log. */ + readonly log: string[]; + /** The time when the job has been started. */ + readonly started: DateTime; + /** The time when the job has been stopped. */ + readonly stopped?: DateTime | undefined; + /** The status of the operation. */ + readonly status: JobStatus; +} + +export class RestoreRequestDto implements IRestoreRequestDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The name of the app. */ + readonly name?: string | undefined; + /** The url to the restore file. */ + readonly url!: string; + + constructor(data?: IRestoreRequestDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).name = _data["name"]; + (this).url = _data["url"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): RestoreRequestDto { + const result = new RestoreRequestDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["name"] = this.name; + data["url"] = this.url; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IRestoreRequestDto { + /** The name of the app. */ + readonly name?: string | undefined; + /** The url to the restore file. */ + readonly url: string; +} + +export type ResizeMode = "Crop" | "CropUpsize" | "Pad" | "BoxPad" | "Max" | "Min" | "Stretch"; + +export const ResizeModeValues: ReadonlyArray = [ + "Crop", + "CropUpsize", + "Pad", + "BoxPad", + "Max", + "Min", + "Stretch" +]; + +export type ImageFormat = "AVIF" | "BMP" | "GIF" | "JPEG" | "PNG" | "TGA" | "TIFF" | "WEBP"; + +export const ImageFormatValues: ReadonlyArray = [ + "AVIF", + "BMP", + "GIF", + "JPEG", + "PNG", + "TGA", + "TIFF", + "WEBP" +]; + +export type WatermarkAnchor = "TopLeft" | "TopRight" | "BottomLeft" | "BottomRight" | "Center"; + +export const WatermarkAnchorValues: ReadonlyArray = [ + "TopLeft", + "TopRight", + "BottomLeft", + "BottomRight", + "Center" +]; + +export class AssetFoldersDto extends ResourceDto implements IAssetFoldersDto { + /** The total number of assets. */ + readonly total!: number; + /** The assets folders. */ + readonly items!: AssetFolderDto[]; + /** The path to the current folder. */ + readonly path!: AssetFolderDto[]; + + get canCreate() { + return this.compute('canCreate', () => hasAnyLink(this._links, 'create')); + } + + constructor(data?: IAssetFoldersDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).total = _data["total"]; + if (Array.isArray(_data["items"])) { + (this).items = [] as any; + for (let item of _data["items"]) + (this).items!.push(AssetFolderDto.fromJSON(item)); + } + if (Array.isArray(_data["path"])) { + (this).path = [] as any; + for (let item of _data["path"]) + (this).path!.push(AssetFolderDto.fromJSON(item)); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): AssetFoldersDto { + const result = new AssetFoldersDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["total"] = this.total; + if (Array.isArray(this.items)) { + data["items"] = []; + for (let item of this.items) + data["items"].push(item.toJSON()); + } + if (Array.isArray(this.path)) { + data["path"] = []; + for (let item of this.path) + data["path"].push(item.toJSON()); + } + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IAssetFoldersDto extends IResourceDto { + /** The total number of assets. */ + readonly total: number; + /** The assets folders. */ + readonly items: AssetFolderDto[]; + /** The path to the current folder. */ + readonly path: AssetFolderDto[]; +} + +export class AssetFolderDto extends ResourceDto implements IAssetFolderDto { + /** The ID of the asset. */ + readonly id!: string; + /** The ID of the parent folder. Empty for files without parent. */ + readonly parentId!: string; + /** The folder name. */ + readonly folderName!: string; + /** The version of the asset folder. */ + readonly version!: number; + + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'update')); + } + + get canMove() { + return this.compute('canMove', () => hasAnyLink(this._links, 'move')); + } + + get canUpdate() { + return this.compute('canUpdate', () => hasAnyLink(this._links, 'update')); + } + + constructor(data?: IAssetFolderDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).id = _data["id"]; + (this).parentId = _data["parentId"]; + (this).folderName = _data["folderName"]; + (this).version = _data["version"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): AssetFolderDto { + const result = new AssetFolderDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["id"] = this.id; + data["parentId"] = this.parentId; + data["folderName"] = this.folderName; + data["version"] = this.version; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IAssetFolderDto extends IResourceDto { + /** The ID of the asset. */ + readonly id: string; + /** The ID of the parent folder. Empty for files without parent. */ + readonly parentId: string; + /** The folder name. */ + readonly folderName: string; + /** The version of the asset folder. */ + readonly version: number; +} + +export type AssetFolderScope = "PathAndItems" | "Path" | "Items"; + +export const AssetFolderScopeValues: ReadonlyArray = [ + "PathAndItems", + "Path", + "Items" +]; + +export class CreateAssetFolderDto implements ICreateAssetFolderDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The name of the folder. */ + readonly folderName!: string; + /** The ID of the parent folder. */ + readonly parentId?: string | undefined; + + constructor(data?: ICreateAssetFolderDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).folderName = _data["folderName"]; + (this).parentId = _data["parentId"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): CreateAssetFolderDto { + const result = new CreateAssetFolderDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["folderName"] = this.folderName; + data["parentId"] = this.parentId; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface ICreateAssetFolderDto { + /** The name of the folder. */ + readonly folderName: string; + /** The ID of the parent folder. */ + readonly parentId?: string | undefined; +} + +export class RenameAssetFolderDto implements IRenameAssetFolderDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The name of the folder. */ + readonly folderName!: string; + + constructor(data?: IRenameAssetFolderDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).folderName = _data["folderName"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): RenameAssetFolderDto { + const result = new RenameAssetFolderDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["folderName"] = this.folderName; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IRenameAssetFolderDto { + /** The name of the folder. */ + readonly folderName: string; +} + +export class MoveAssetFolderDto implements IMoveAssetFolderDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The parent folder id. */ + readonly parentId?: string | undefined; + + constructor(data?: IMoveAssetFolderDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).parentId = _data["parentId"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): MoveAssetFolderDto { + const result = new MoveAssetFolderDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["parentId"] = this.parentId; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IMoveAssetFolderDto { + /** The parent folder id. */ + readonly parentId?: string | undefined; +} + +export class RenameTagDto implements IRenameTagDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The new name for the tag. */ + readonly tagName!: string; + + constructor(data?: IRenameTagDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).tagName = _data["tagName"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): RenameTagDto { + const result = new RenameTagDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["tagName"] = this.tagName; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IRenameTagDto { + /** The new name for the tag. */ + readonly tagName: string; +} + +export class AssetsDto extends ResourceDto implements IAssetsDto { + /** The total number of assets. */ + readonly total!: number; + /** The assets. */ + readonly items!: AssetDto[]; + + get canCreate() { + return this.compute('canCreate', () => hasAnyLink(this._links, 'create')); + } + + get canRenameTag() { + return this.compute('canRenameTag', () => hasAnyLink(this._links, 'tags/rename')); + } + + constructor(data?: IAssetsDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).total = _data["total"]; + if (Array.isArray(_data["items"])) { + (this).items = [] as any; + for (let item of _data["items"]) + (this).items!.push(AssetDto.fromJSON(item)); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): AssetsDto { + const result = new AssetsDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["total"] = this.total; + if (Array.isArray(this.items)) { + data["items"] = []; + for (let item of this.items) + data["items"].push(item.toJSON()); + } + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IAssetsDto extends IResourceDto { + /** The total number of assets. */ + readonly total: number; + /** The assets. */ + readonly items: AssetDto[]; +} + +export class AssetDto extends ResourceDto implements IAssetDto { + /** The ID of the asset. */ + readonly id!: string; + /** The ID of the parent folder. Empty for files without parent. */ + readonly parentId!: string; + /** The file name. */ + readonly fileName!: string; + /** The file hash. */ + readonly fileHash?: string | undefined; + /** True, when the asset is not public. */ + readonly isProtected!: boolean; + /** The slug. */ + readonly slug!: string; + /** The mime type. */ + readonly mimeType!: string; + /** The file type. */ + readonly fileType!: string; + /** The formatted text representation of the metadata. */ + readonly metadataText!: string; + /** The UI token. */ + readonly editToken?: string | undefined; + /** The asset metadata. */ + readonly metadata!: { [key: string]: any; }; + /** The asset tags. */ + readonly tags!: string[]; + /** The size of the file in bytes. */ + readonly fileSize!: number; + /** The version of the file. */ + readonly fileVersion!: number; + /** The type of the asset. */ + readonly type!: AssetType; + /** The user that has created the schema. */ + readonly createdBy!: string; + /** The user that has updated the asset. */ + readonly lastModifiedBy!: string; + /** The date and time when the asset has been created. */ + readonly created!: DateTime; + /** The date and time when the asset has been modified last. */ + readonly lastModified!: DateTime; + /** The version of the asset. */ + readonly version!: number; + /** The metadata. */ + readonly _meta?: AssetMetaDto | undefined; + /** Determines of the created file is an image. */ + readonly isImage!: boolean; + /** The width of the image in pixels if the asset is an image. */ + readonly pixelWidth?: number | undefined; + /** The height of the image in pixels if the asset is an image. */ + readonly pixelHeight?: number | undefined; + + public get isDuplicate() { + return this.compute('isDuplicate', () => this._meta && this._meta['isDuplicate'] === 'true'); + } + + public get contentUrl() { + return this.compute('contentUrl', () => this._links['content']?.href); + } + + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'update')); + } + + get canMove() { + return this.compute('canMove', () => hasAnyLink(this._links, 'move')); + } + + get canUpdate() { + return this.compute('canUpdate', () => hasAnyLink(this._links, 'update')); + } + + get canUpload() { + return this.compute('canUpload', () => hasAnyLink(this._links, 'upload')); + } + + get canPreview() { + return this.compute('canPreview', () => { + const SVG_PREVIEW_LIMIT = 10 * 1024; + const MIME_TIFF = 'image/tiff'; + const MIME_SVG = 'image/svg+xml'; + + const canPreview = + (this.mimeType !== MIME_TIFF && this.type === 'Image') || + (this.mimeType === MIME_SVG && this.fileSize < SVG_PREVIEW_LIMIT); + + return canPreview; + }); + } + + public get fileNameWithoutExtension() { + return this.compute('fileNameWithoutExtension', () => { + const index = this.fileName.lastIndexOf('.'); + + if (index > 0) { + return this.fileName.substring(0, index); + } else { + return this.fileName; + } + + }); + } + + public fullUrl(apiUrl: ApiUrlConfig) { + return apiUrl.buildUrl(this.contentUrl); + } + + constructor(data?: IAssetDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).id = _data["id"]; + (this).parentId = _data["parentId"]; + (this).fileName = _data["fileName"]; + (this).fileHash = _data["fileHash"]; + (this).isProtected = _data["isProtected"]; + (this).slug = _data["slug"]; + (this).mimeType = _data["mimeType"]; + (this).fileType = _data["fileType"]; + (this).metadataText = _data["metadataText"]; + (this).editToken = _data["editToken"]; + if (_data["metadata"]) { + (this).metadata = {} as any; + for (let key in _data["metadata"]) { + if (_data["metadata"].hasOwnProperty(key)) + ((this).metadata)![key] = _data["metadata"][key]; + } + } + if (Array.isArray(_data["tags"])) { + (this).tags = [] as any; + for (let item of _data["tags"]) + (this).tags!.push(item); + } + (this).fileSize = _data["fileSize"]; + (this).fileVersion = _data["fileVersion"]; + (this).type = _data["type"]; + (this).createdBy = _data["createdBy"]; + (this).lastModifiedBy = _data["lastModifiedBy"]; + (this).created = _data["created"] ? DateTime.parseISO(_data["created"].toString()) : undefined; + (this).lastModified = _data["lastModified"] ? DateTime.parseISO(_data["lastModified"].toString()) : undefined; + (this).version = _data["version"]; + (this)._meta = _data["_meta"] ? AssetMetaDto.fromJSON(_data["_meta"]) : undefined; + (this).isImage = _data["isImage"]; + (this).pixelWidth = _data["pixelWidth"]; + (this).pixelHeight = _data["pixelHeight"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): AssetDto { + const result = new AssetDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["id"] = this.id; + data["parentId"] = this.parentId; + data["fileName"] = this.fileName; + data["fileHash"] = this.fileHash; + data["isProtected"] = this.isProtected; + data["slug"] = this.slug; + data["mimeType"] = this.mimeType; + data["fileType"] = this.fileType; + data["metadataText"] = this.metadataText; + data["editToken"] = this.editToken; + if (this.metadata) { + data["metadata"] = {}; + for (let key in this.metadata) { + if (this.metadata.hasOwnProperty(key)) + (data["metadata"])[key] = (this.metadata)[key]; + } + } + if (Array.isArray(this.tags)) { + data["tags"] = []; + for (let item of this.tags) + data["tags"].push(item); + } + data["fileSize"] = this.fileSize; + data["fileVersion"] = this.fileVersion; + data["type"] = this.type; + data["createdBy"] = this.createdBy; + data["lastModifiedBy"] = this.lastModifiedBy; + data["created"] = this.created ? this.created.toISOString() : undefined; + data["lastModified"] = this.lastModified ? this.lastModified.toISOString() : undefined; + data["version"] = this.version; + data["_meta"] = this._meta ? this._meta.toJSON() : undefined; + data["isImage"] = this.isImage; + data["pixelWidth"] = this.pixelWidth; + data["pixelHeight"] = this.pixelHeight; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IAssetDto extends IResourceDto { + /** The ID of the asset. */ + readonly id: string; + /** The ID of the parent folder. Empty for files without parent. */ + readonly parentId: string; + /** The file name. */ + readonly fileName: string; + /** The file hash. */ + readonly fileHash?: string | undefined; + /** True, when the asset is not public. */ + readonly isProtected: boolean; + /** The slug. */ + readonly slug: string; + /** The mime type. */ + readonly mimeType: string; + /** The file type. */ + readonly fileType: string; + /** The formatted text representation of the metadata. */ + readonly metadataText: string; + /** The UI token. */ + readonly editToken?: string | undefined; + /** The asset metadata. */ + readonly metadata: { [key: string]: any; }; + /** The asset tags. */ + readonly tags: string[]; + /** The size of the file in bytes. */ + readonly fileSize: number; + /** The version of the file. */ + readonly fileVersion: number; + /** The type of the asset. */ + readonly type: AssetType; + /** The user that has created the schema. */ + readonly createdBy: string; + /** The user that has updated the asset. */ + readonly lastModifiedBy: string; + /** The date and time when the asset has been created. */ + readonly created: DateTime; + /** The date and time when the asset has been modified last. */ + readonly lastModified: DateTime; + /** The version of the asset. */ + readonly version: number; + /** The metadata. */ + readonly _meta?: AssetMetaDto | undefined; + /** Determines of the created file is an image. */ + readonly isImage: boolean; + /** The width of the image in pixels if the asset is an image. */ + readonly pixelWidth?: number | undefined; + /** The height of the image in pixels if the asset is an image. */ + readonly pixelHeight?: number | undefined; +} + +export class AssetMetaDto implements IAssetMetaDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** Indicates whether the asset is a duplicate. */ + readonly isDuplicate!: string; + + constructor(data?: IAssetMetaDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).isDuplicate = _data["isDuplicate"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): AssetMetaDto { + const result = new AssetMetaDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["isDuplicate"] = this.isDuplicate; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IAssetMetaDto { + /** Indicates whether the asset is a duplicate. */ + readonly isDuplicate: string; +} + +export class BulkUpdateAssetsDto implements IBulkUpdateAssetsDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The contents to update or insert. */ + readonly jobs?: BulkUpdateAssetsJobDto[] | undefined; + /** True to check referrers of deleted assets. */ + readonly checkReferrers?: boolean; + /** True to turn off costly validation: Folder checks. Default: true. */ + readonly optimizeValidation?: boolean; + /** True to turn off scripting for faster inserts. Default: true. */ + readonly doNotScript?: boolean; + + constructor(data?: IBulkUpdateAssetsDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + if (Array.isArray(_data["jobs"])) { + (this).jobs = [] as any; + for (let item of _data["jobs"]) + (this).jobs!.push(BulkUpdateAssetsJobDto.fromJSON(item)); + } + (this).checkReferrers = _data["checkReferrers"]; + (this).optimizeValidation = _data["optimizeValidation"]; + (this).doNotScript = _data["doNotScript"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): BulkUpdateAssetsDto { + const result = new BulkUpdateAssetsDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (Array.isArray(this.jobs)) { + data["jobs"] = []; + for (let item of this.jobs) + data["jobs"].push(item.toJSON()); + } + data["checkReferrers"] = this.checkReferrers; + data["optimizeValidation"] = this.optimizeValidation; + data["doNotScript"] = this.doNotScript; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IBulkUpdateAssetsDto { + /** The contents to update or insert. */ + readonly jobs?: BulkUpdateAssetsJobDto[] | undefined; + /** True to check referrers of deleted assets. */ + readonly checkReferrers?: boolean; + /** True to turn off costly validation: Folder checks. Default: true. */ + readonly optimizeValidation?: boolean; + /** True to turn off scripting for faster inserts. Default: true. */ + readonly doNotScript?: boolean; +} + +export class BulkUpdateAssetsJobDto implements IBulkUpdateAssetsJobDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** An optional ID of the asset to update. */ + readonly id?: string | undefined; + /** The update type. */ + readonly type?: BulkUpdateAssetType; + /** The parent folder id. */ + readonly parentId?: string | undefined; + /** The new name of the asset. */ + readonly fileName?: string | undefined; + /** The new slug of the asset. */ + readonly slug?: string | undefined; + /** True, when the asset is not public. */ + readonly isProtected?: boolean | undefined; + /** The new asset tags. */ + readonly tags?: string[] | undefined; + /** The asset metadata. */ + readonly metadata?: { [key: string]: any; } | undefined; + /** True to delete the asset permanently. */ + readonly permanent?: boolean; + /** The expected version. */ + readonly expectedVersion?: number; + + constructor(data?: IBulkUpdateAssetsJobDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).id = _data["id"]; + (this).type = _data["type"]; + (this).parentId = _data["parentId"]; + (this).fileName = _data["fileName"]; + (this).slug = _data["slug"]; + (this).isProtected = _data["isProtected"]; + if (Array.isArray(_data["tags"])) { + (this).tags = [] as any; + for (let item of _data["tags"]) + (this).tags!.push(item); + } + if (_data["metadata"]) { + (this).metadata = {} as any; + for (let key in _data["metadata"]) { + if (_data["metadata"].hasOwnProperty(key)) + ((this).metadata)![key] = _data["metadata"][key]; + } + } + (this).permanent = _data["permanent"]; + (this).expectedVersion = _data["expectedVersion"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): BulkUpdateAssetsJobDto { + const result = new BulkUpdateAssetsJobDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["id"] = this.id; + data["type"] = this.type; + data["parentId"] = this.parentId; + data["fileName"] = this.fileName; + data["slug"] = this.slug; + data["isProtected"] = this.isProtected; + if (Array.isArray(this.tags)) { + data["tags"] = []; + for (let item of this.tags) + data["tags"].push(item); + } + if (this.metadata) { + data["metadata"] = {}; + for (let key in this.metadata) { + if (this.metadata.hasOwnProperty(key)) + (data["metadata"])[key] = (this.metadata)[key]; + } + } + data["permanent"] = this.permanent; + data["expectedVersion"] = this.expectedVersion; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IBulkUpdateAssetsJobDto { + /** An optional ID of the asset to update. */ + readonly id?: string | undefined; + /** The update type. */ + readonly type?: BulkUpdateAssetType; + /** The parent folder id. */ + readonly parentId?: string | undefined; + /** The new name of the asset. */ + readonly fileName?: string | undefined; + /** The new slug of the asset. */ + readonly slug?: string | undefined; + /** True, when the asset is not public. */ + readonly isProtected?: boolean | undefined; + /** The new asset tags. */ + readonly tags?: string[] | undefined; + /** The asset metadata. */ + readonly metadata?: { [key: string]: any; } | undefined; + /** True to delete the asset permanently. */ + readonly permanent?: boolean; + /** The expected version. */ + readonly expectedVersion?: number; +} + +export type BulkUpdateAssetType = "Annotate" | "Move" | "Delete"; + +export const BulkUpdateAssetTypeValues: ReadonlyArray = [ + "Annotate", + "Move", + "Delete" +]; + +export class AnnotateAssetDto implements IAnnotateAssetDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The new name of the asset. */ + readonly fileName?: string | undefined; + /** The new slug of the asset. */ + readonly slug?: string | undefined; + /** True, when the asset is not public. */ + readonly isProtected?: boolean | undefined; + /** The new asset tags. */ + readonly tags?: string[] | undefined; + /** The asset metadata. */ + readonly metadata?: { [key: string]: any; } | undefined; + + constructor(data?: IAnnotateAssetDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).fileName = _data["fileName"]; + (this).slug = _data["slug"]; + (this).isProtected = _data["isProtected"]; + if (Array.isArray(_data["tags"])) { + (this).tags = [] as any; + for (let item of _data["tags"]) + (this).tags!.push(item); + } + if (_data["metadata"]) { + (this).metadata = {} as any; + for (let key in _data["metadata"]) { + if (_data["metadata"].hasOwnProperty(key)) + ((this).metadata)![key] = _data["metadata"][key]; + } + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): AnnotateAssetDto { + const result = new AnnotateAssetDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["fileName"] = this.fileName; + data["slug"] = this.slug; + data["isProtected"] = this.isProtected; + if (Array.isArray(this.tags)) { + data["tags"] = []; + for (let item of this.tags) + data["tags"].push(item); + } + if (this.metadata) { + data["metadata"] = {}; + for (let key in this.metadata) { + if (this.metadata.hasOwnProperty(key)) + (data["metadata"])[key] = (this.metadata)[key]; + } + } + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IAnnotateAssetDto { + /** The new name of the asset. */ + readonly fileName?: string | undefined; + /** The new slug of the asset. */ + readonly slug?: string | undefined; + /** True, when the asset is not public. */ + readonly isProtected?: boolean | undefined; + /** The new asset tags. */ + readonly tags?: string[] | undefined; + /** The asset metadata. */ + readonly metadata?: { [key: string]: any; } | undefined; +} + +export class MoveAssetDto implements IMoveAssetDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The parent folder id. */ + readonly parentId?: string | undefined; + + constructor(data?: IMoveAssetDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).parentId = _data["parentId"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): MoveAssetDto { + const result = new MoveAssetDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["parentId"] = this.parentId; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IMoveAssetDto { + /** The parent folder id. */ + readonly parentId?: string | undefined; +} + +export class AssetScriptsDto extends ResourceDto implements IAssetScriptsDto { + /** The script that is executed for each asset when querying assets. */ + readonly query?: string | undefined; + /** The script that is executed for all assets when querying assets. */ + readonly queryPre?: string | undefined; + /** The script that is executed when creating an asset. */ + readonly create?: string | undefined; + /** The script that is executed when updating a content. */ + readonly update?: string | undefined; + /** The script that is executed when annotating a content. */ + readonly annotate?: string | undefined; + /** The script that is executed when moving a content. */ + readonly move?: string | undefined; + /** The script that is executed when deleting a content. */ + readonly delete?: string | undefined; + /** The version of the app. */ + readonly version!: number; + + get canUpdate() { + return this.compute('canUpdate', () => hasAnyLink(this._links, 'update')); + } + + constructor(data?: IAssetScriptsDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).query = _data["query"]; + (this).queryPre = _data["queryPre"]; + (this).create = _data["create"]; + (this).update = _data["update"]; + (this).annotate = _data["annotate"]; + (this).move = _data["move"]; + (this).delete = _data["delete"]; + (this).version = _data["version"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): AssetScriptsDto { + const result = new AssetScriptsDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["query"] = this.query; + data["queryPre"] = this.queryPre; + data["create"] = this.create; + data["update"] = this.update; + data["annotate"] = this.annotate; + data["move"] = this.move; + data["delete"] = this.delete; + data["version"] = this.version; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IAssetScriptsDto extends IResourceDto { + /** The script that is executed for each asset when querying assets. */ + readonly query?: string | undefined; + /** The script that is executed for all assets when querying assets. */ + readonly queryPre?: string | undefined; + /** The script that is executed when creating an asset. */ + readonly create?: string | undefined; + /** The script that is executed when updating a content. */ + readonly update?: string | undefined; + /** The script that is executed when annotating a content. */ + readonly annotate?: string | undefined; + /** The script that is executed when moving a content. */ + readonly move?: string | undefined; + /** The script that is executed when deleting a content. */ + readonly delete?: string | undefined; + /** The version of the app. */ + readonly version: number; +} + +export class UpdateAssetScriptsDto implements IUpdateAssetScriptsDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The script that is executed for each asset when querying assets. */ + readonly query?: string | undefined; + /** The script that is executed for all assets when querying assets. */ + readonly queryPre?: string | undefined; + /** The script that is executed when creating an asset. */ + readonly create?: string | undefined; + /** The script that is executed when updating a content. */ + readonly update?: string | undefined; + /** The script that is executed when annotating a content. */ + readonly annotate?: string | undefined; + /** The script that is executed when moving a content. */ + readonly move?: string | undefined; + /** The script that is executed when deleting a content. */ + readonly delete?: string | undefined; + + constructor(data?: IUpdateAssetScriptsDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).query = _data["query"]; + (this).queryPre = _data["queryPre"]; + (this).create = _data["create"]; + (this).update = _data["update"]; + (this).annotate = _data["annotate"]; + (this).move = _data["move"]; + (this).delete = _data["delete"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): UpdateAssetScriptsDto { + const result = new UpdateAssetScriptsDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["query"] = this.query; + data["queryPre"] = this.queryPre; + data["create"] = this.create; + data["update"] = this.update; + data["annotate"] = this.annotate; + data["move"] = this.move; + data["delete"] = this.delete; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IUpdateAssetScriptsDto { + /** The script that is executed for each asset when querying assets. */ + readonly query?: string | undefined; + /** The script that is executed for all assets when querying assets. */ + readonly queryPre?: string | undefined; + /** The script that is executed when creating an asset. */ + readonly create?: string | undefined; + /** The script that is executed when updating a content. */ + readonly update?: string | undefined; + /** The script that is executed when annotating a content. */ + readonly annotate?: string | undefined; + /** The script that is executed when moving a content. */ + readonly move?: string | undefined; + /** The script that is executed when deleting a content. */ + readonly delete?: string | undefined; +} + +export class ClientsDto extends ResourceDto implements IClientsDto { + /** The clients. */ + readonly items!: ClientDto[]; + + get canCreate() { + return this.compute('canCreate', () => hasAnyLink(this._links, 'create')); + } + + constructor(data?: IClientsDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + if (Array.isArray(_data["items"])) { + (this).items = [] as any; + for (let item of _data["items"]) + (this).items!.push(ClientDto.fromJSON(item)); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): ClientsDto { + const result = new ClientsDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (Array.isArray(this.items)) { + data["items"] = []; + for (let item of this.items) + data["items"].push(item.toJSON()); + } + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IClientsDto extends IResourceDto { + /** The clients. */ + readonly items: ClientDto[]; +} + +export class ClientDto extends ResourceDto implements IClientDto { + /** The client id. */ + readonly id!: string; + /** The client secret. */ + readonly secret!: string; + /** The client name. */ + readonly name!: string; + /** The role of the client. */ + readonly role?: string | undefined; + /** The number of allowed api calls per month for this client. */ + readonly apiCallsLimit!: number; + /** The number of allowed api traffic bytes per month for this client. */ + readonly apiTrafficLimit!: number; + /** True to allow anonymous access without an access token for this client. */ + readonly allowAnonymous!: boolean; + + get canRevoke() { + return this.compute('canRevoke', () => hasAnyLink(this._links, 'delete')); + } + + get canUpdate() { + return this.compute('canUpdate', () => hasAnyLink(this._links, 'update')); + } + + constructor(data?: IClientDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).id = _data["id"]; + (this).secret = _data["secret"]; + (this).name = _data["name"]; + (this).role = _data["role"]; + (this).apiCallsLimit = _data["apiCallsLimit"]; + (this).apiTrafficLimit = _data["apiTrafficLimit"]; + (this).allowAnonymous = _data["allowAnonymous"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): ClientDto { + const result = new ClientDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["id"] = this.id; + data["secret"] = this.secret; + data["name"] = this.name; + data["role"] = this.role; + data["apiCallsLimit"] = this.apiCallsLimit; + data["apiTrafficLimit"] = this.apiTrafficLimit; + data["allowAnonymous"] = this.allowAnonymous; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IClientDto extends IResourceDto { + /** The client id. */ + readonly id: string; + /** The client secret. */ + readonly secret: string; + /** The client name. */ + readonly name: string; + /** The role of the client. */ + readonly role?: string | undefined; + /** The number of allowed api calls per month for this client. */ + readonly apiCallsLimit: number; + /** The number of allowed api traffic bytes per month for this client. */ + readonly apiTrafficLimit: number; + /** True to allow anonymous access without an access token for this client. */ + readonly allowAnonymous: boolean; +} + +export class CreateClientDto implements ICreateClientDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The ID of the client. */ + readonly id!: string; + + constructor(data?: ICreateClientDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).id = _data["id"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): CreateClientDto { + const result = new CreateClientDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["id"] = this.id; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface ICreateClientDto { + /** The ID of the client. */ + readonly id: string; +} + +export class UpdateClientDto implements IUpdateClientDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The new display name of the client. */ + readonly name?: string | undefined; + /** The role of the client. */ + readonly role?: string | undefined; + /** True to allow anonymous access without an access token for this client. */ + readonly allowAnonymous?: boolean | undefined; + /** The number of allowed api calls per month for this client. */ + readonly apiCallsLimit?: number | undefined; + /** The number of allowed api traffic bytes per month for this client. */ + readonly apiTrafficLimit?: number | undefined; + + constructor(data?: IUpdateClientDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).name = _data["name"]; + (this).role = _data["role"]; + (this).allowAnonymous = _data["allowAnonymous"]; + (this).apiCallsLimit = _data["apiCallsLimit"]; + (this).apiTrafficLimit = _data["apiTrafficLimit"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): UpdateClientDto { + const result = new UpdateClientDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["name"] = this.name; + data["role"] = this.role; + data["allowAnonymous"] = this.allowAnonymous; + data["apiCallsLimit"] = this.apiCallsLimit; + data["apiTrafficLimit"] = this.apiTrafficLimit; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IUpdateClientDto { + /** The new display name of the client. */ + readonly name?: string | undefined; + /** The role of the client. */ + readonly role?: string | undefined; + /** True to allow anonymous access without an access token for this client. */ + readonly allowAnonymous?: boolean | undefined; + /** The number of allowed api calls per month for this client. */ + readonly apiCallsLimit?: number | undefined; + /** The number of allowed api traffic bytes per month for this client. */ + readonly apiTrafficLimit?: number | undefined; +} + +export class AppLanguagesDto extends ResourceDto implements IAppLanguagesDto { + /** The languages. */ + readonly items!: AppLanguageDto[]; + + get canCreate() { + return this.compute('canCreate', () => hasAnyLink(this._links, 'create')); + } + + constructor(data?: IAppLanguagesDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + if (Array.isArray(_data["items"])) { + (this).items = [] as any; + for (let item of _data["items"]) + (this).items!.push(AppLanguageDto.fromJSON(item)); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): AppLanguagesDto { + const result = new AppLanguagesDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (Array.isArray(this.items)) { + data["items"] = []; + for (let item of this.items) + data["items"].push(item.toJSON()); + } + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IAppLanguagesDto extends IResourceDto { + /** The languages. */ + readonly items: AppLanguageDto[]; +} + +export class AppLanguageDto extends ResourceDto implements IAppLanguageDto { + /** The iso code of the language. */ + readonly iso2Code!: string; + /** The english name of the language. */ + readonly englishName!: string; + /** The fallback languages. */ + readonly fallback!: string[]; + /** Indicates if the language is the master language. */ + readonly isMaster!: boolean; + /** Indicates if the language is optional. */ + readonly isOptional!: boolean; + + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'delete')); + } + + get canUpdate() { + return this.compute('canUpdate', () => hasAnyLink(this._links, 'update')); + } + + constructor(data?: IAppLanguageDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).iso2Code = _data["iso2Code"]; + (this).englishName = _data["englishName"]; + if (Array.isArray(_data["fallback"])) { + (this).fallback = [] as any; + for (let item of _data["fallback"]) + (this).fallback!.push(item); + } + (this).isMaster = _data["isMaster"]; + (this).isOptional = _data["isOptional"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): AppLanguageDto { + const result = new AppLanguageDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["iso2Code"] = this.iso2Code; + data["englishName"] = this.englishName; + if (Array.isArray(this.fallback)) { + data["fallback"] = []; + for (let item of this.fallback) + data["fallback"].push(item); + } + data["isMaster"] = this.isMaster; + data["isOptional"] = this.isOptional; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IAppLanguageDto extends IResourceDto { + /** The iso code of the language. */ + readonly iso2Code: string; + /** The english name of the language. */ + readonly englishName: string; + /** The fallback languages. */ + readonly fallback: string[]; + /** Indicates if the language is the master language. */ + readonly isMaster: boolean; + /** Indicates if the language is optional. */ + readonly isOptional: boolean; +} + +export class AddLanguageDto implements IAddLanguageDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The language to add. */ + readonly language!: string; + + constructor(data?: IAddLanguageDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).language = _data["language"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): AddLanguageDto { + const result = new AddLanguageDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["language"] = this.language; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IAddLanguageDto { + /** The language to add. */ + readonly language: string; +} + +export class UpdateLanguageDto implements IUpdateLanguageDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** Set the value to true to make the language the master. */ + readonly isMaster?: boolean | undefined; + /** Set the value to true to make the language optional. */ + readonly isOptional?: boolean; + /** Optional fallback languages. */ + readonly fallback?: string[] | undefined; + + constructor(data?: IUpdateLanguageDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).isMaster = _data["isMaster"]; + (this).isOptional = _data["isOptional"]; + if (Array.isArray(_data["fallback"])) { + (this).fallback = [] as any; + for (let item of _data["fallback"]) + (this).fallback!.push(item); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): UpdateLanguageDto { + const result = new UpdateLanguageDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["isMaster"] = this.isMaster; + data["isOptional"] = this.isOptional; + if (Array.isArray(this.fallback)) { + data["fallback"] = []; + for (let item of this.fallback) + data["fallback"].push(item); + } + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IUpdateLanguageDto { + /** Set the value to true to make the language the master. */ + readonly isMaster?: boolean | undefined; + /** Set the value to true to make the language optional. */ + readonly isOptional?: boolean; + /** Optional fallback languages. */ + readonly fallback?: string[] | undefined; +} + +export class RolesDto extends ResourceDto implements IRolesDto { + /** The roles. */ + readonly items!: RoleDto[]; + + get canCreate() { + return this.compute('canCreate', () => hasAnyLink(this._links, 'create')); + } + + constructor(data?: IRolesDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + if (Array.isArray(_data["items"])) { + (this).items = [] as any; + for (let item of _data["items"]) + (this).items!.push(RoleDto.fromJSON(item)); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): RolesDto { + const result = new RolesDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (Array.isArray(this.items)) { + data["items"] = []; + for (let item of this.items) + data["items"].push(item.toJSON()); + } + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IRolesDto extends IResourceDto { + /** The roles. */ + readonly items: RoleDto[]; +} + +export class RoleDto extends ResourceDto implements IRoleDto { + /** The role name. */ + readonly name!: string; + /** The number of clients with this role. */ + readonly numClients!: number; + /** The number of contributors with this role. */ + readonly numContributors!: number; + /** Indicates if the role is an builtin default role. */ + readonly isDefaultRole!: boolean; + /** Associated list of permissions. */ + readonly permissions!: string[]; + /** Associated list of UI properties. */ + readonly properties!: { [key: string]: any; }; + + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'update')); + } + + get canUpdate() { + return this.compute('canUpdate', () => hasAnyLink(this._links, 'update')); + } + + constructor(data?: IRoleDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).name = _data["name"]; + (this).numClients = _data["numClients"]; + (this).numContributors = _data["numContributors"]; + (this).isDefaultRole = _data["isDefaultRole"]; + if (Array.isArray(_data["permissions"])) { + (this).permissions = [] as any; + for (let item of _data["permissions"]) + (this).permissions!.push(item); + } + if (_data["properties"]) { + (this).properties = {} as any; + for (let key in _data["properties"]) { + if (_data["properties"].hasOwnProperty(key)) + ((this).properties)![key] = _data["properties"][key]; + } + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): RoleDto { + const result = new RoleDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["name"] = this.name; + data["numClients"] = this.numClients; + data["numContributors"] = this.numContributors; + data["isDefaultRole"] = this.isDefaultRole; + if (Array.isArray(this.permissions)) { + data["permissions"] = []; + for (let item of this.permissions) + data["permissions"].push(item); + } + if (this.properties) { + data["properties"] = {}; + for (let key in this.properties) { + if (this.properties.hasOwnProperty(key)) + (data["properties"])[key] = (this.properties)[key]; + } + } + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IRoleDto extends IResourceDto { + /** The role name. */ + readonly name: string; + /** The number of clients with this role. */ + readonly numClients: number; + /** The number of contributors with this role. */ + readonly numContributors: number; + /** Indicates if the role is an builtin default role. */ + readonly isDefaultRole: boolean; + /** Associated list of permissions. */ + readonly permissions: string[]; + /** Associated list of UI properties. */ + readonly properties: { [key: string]: any; }; +} + +export class AddRoleDto implements IAddRoleDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The role name. */ + readonly name!: string; + + constructor(data?: IAddRoleDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).name = _data["name"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): AddRoleDto { + const result = new AddRoleDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["name"] = this.name; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IAddRoleDto { + /** The role name. */ + readonly name: string; +} + +export class UpdateRoleDto implements IUpdateRoleDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** Associated list of permissions. */ + readonly permissions!: string[]; + /** Associated list of UI properties. */ + readonly properties?: { [key: string]: any; }; + + constructor(data?: IUpdateRoleDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + if (Array.isArray(_data["permissions"])) { + (this).permissions = [] as any; + for (let item of _data["permissions"]) + (this).permissions!.push(item); + } + if (_data["properties"]) { + (this).properties = {} as any; + for (let key in _data["properties"]) { + if (_data["properties"].hasOwnProperty(key)) + ((this).properties)![key] = _data["properties"][key]; + } + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): UpdateRoleDto { + const result = new UpdateRoleDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (Array.isArray(this.permissions)) { + data["permissions"] = []; + for (let item of this.permissions) + data["permissions"].push(item); + } + if (this.properties) { + data["properties"] = {}; + for (let key in this.properties) { + if (this.properties.hasOwnProperty(key)) + (data["properties"])[key] = (this.properties)[key]; + } + } + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IUpdateRoleDto { + /** Associated list of permissions. */ + readonly permissions: string[]; + /** Associated list of UI properties. */ + readonly properties?: { [key: string]: any; }; +} + +export class AppDto extends ResourceDto implements IAppDto { + /** The user that has created the app. */ + readonly createdBy?: string | undefined; + /** The user that has updated the app. */ + readonly lastModifiedBy?: string | undefined; + /** The ID of the app. */ + readonly id!: string; + /** The name of the app. */ + readonly name!: string; + /** The optional label of the app. */ + readonly label?: string | undefined; + /** The optional description of the app. */ + readonly description?: string | undefined; + /** The version of the app. */ + readonly version!: number; + /** The timestamp when the app has been created. */ + readonly created!: DateTime; + /** The timestamp when the app has been modified last. */ + readonly lastModified!: DateTime; + /** The ID of the team. */ + readonly teamId?: string | undefined; + /** The permission level of the user. */ + readonly permissions!: string[]; + /** Indicates if the user can access the api. */ + readonly canAccessApi!: boolean; + /** Indicates if the user can access at least one content. */ + readonly canAccessContent!: boolean; + /** The role name of the user. */ + readonly roleName?: string | undefined; + /** The properties from the role. */ + readonly roleProperties!: { [key: string]: any; }; + + get displayName() { + return this.compute('displayName', () => StringHelper.firstNonEmpty(this.label, this.name)); + } + + get canCreateSchema() { + return this.compute('canCreateSchema', () => hasAnyLink(this._links, 'schemas/create')); + } + + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'delete')); + } + + get canLeave() { + return this.compute('canLeave', () => hasAnyLink(this._links, 'leave')); + } + + get canReadAssets() { + return this.compute('canReadAssets', () => hasAnyLink(this._links, 'assets')); + } + + get canReadAssetsScripts() { + return this.compute('canReadAssetsScripts', () => hasAnyLink(this._links, 'assets/scripts')); + } + + get canReadClients() { + return this.compute('canReadClients', () => hasAnyLink(this._links, 'clients')); + } + + get canReadContributors() { + return this.compute('canReadContributors', () => hasAnyLink(this._links, 'contributors')); + } + + get canReadJobs() { + return this.compute('canReadJobs', () => hasAnyLink(this._links, 'jobs')); + } + + get canReadLanguages() { + return this.compute('canReadLanguages', () => hasAnyLink(this._links, 'languages')); + } + + get canReadPatterns() { + return this.compute('canReadPatterns', () => hasAnyLink(this._links, 'patterns')); + } + + get canReadPlans() { + return this.compute('canReadPlans', () => hasAnyLink(this._links, 'plans')); + } + + get canReadRoles() { + return this.compute('canReadRoles', () => hasAnyLink(this._links, 'roles')); + } + + get canReadRules() { + return this.compute('canReadRules', () => hasAnyLink(this._links, 'rules')); + } + + get canReadSchemas() { + return this.compute('canReadSchemas', () => hasAnyLink(this._links, 'schemas')); + } + + get canReadWorkflows() { + return this.compute('canReadWorkflows', () => hasAnyLink(this._links, 'workflows')); + } + + get canUpdateGeneral() { + return this.compute('canUpdateGeneral', () => hasAnyLink(this._links, 'update')); + } + + get canUpdateImage() { + return this.compute('canUpdateImage', () => hasAnyLink(this._links, 'image/upload')); + } + + get canUpdateTeam() { + return this.compute('canUpdateTeam', () => hasAnyLink(this._links, 'transfer')); + } + + get canUploadAssets() { + return this.compute('canUploadAssets', () => hasAnyLink(this._links, 'assets/create')); + } + + get image() { + return this.compute('image', () => this._links['image']?.href); + } + + constructor(data?: IAppDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).createdBy = _data["createdBy"]; + (this).lastModifiedBy = _data["lastModifiedBy"]; + (this).id = _data["id"]; + (this).name = _data["name"]; + (this).label = _data["label"]; + (this).description = _data["description"]; + (this).version = _data["version"]; + (this).created = _data["created"] ? DateTime.parseISO(_data["created"].toString()) : undefined; + (this).lastModified = _data["lastModified"] ? DateTime.parseISO(_data["lastModified"].toString()) : undefined; + (this).teamId = _data["teamId"]; + if (Array.isArray(_data["permissions"])) { + (this).permissions = [] as any; + for (let item of _data["permissions"]) + (this).permissions!.push(item); + } + (this).canAccessApi = _data["canAccessApi"]; + (this).canAccessContent = _data["canAccessContent"]; + (this).roleName = _data["roleName"]; + if (_data["roleProperties"]) { + (this).roleProperties = {} as any; + for (let key in _data["roleProperties"]) { + if (_data["roleProperties"].hasOwnProperty(key)) + ((this).roleProperties)![key] = _data["roleProperties"][key]; + } + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): AppDto { + const result = new AppDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["createdBy"] = this.createdBy; + data["lastModifiedBy"] = this.lastModifiedBy; + data["id"] = this.id; + data["name"] = this.name; + data["label"] = this.label; + data["description"] = this.description; + data["version"] = this.version; + data["created"] = this.created ? this.created.toISOString() : undefined; + data["lastModified"] = this.lastModified ? this.lastModified.toISOString() : undefined; + data["teamId"] = this.teamId; + if (Array.isArray(this.permissions)) { + data["permissions"] = []; + for (let item of this.permissions) + data["permissions"].push(item); + } + data["canAccessApi"] = this.canAccessApi; + data["canAccessContent"] = this.canAccessContent; + data["roleName"] = this.roleName; + if (this.roleProperties) { + data["roleProperties"] = {}; + for (let key in this.roleProperties) { + if (this.roleProperties.hasOwnProperty(key)) + (data["roleProperties"])[key] = (this.roleProperties)[key]; + } + } + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IAppDto extends IResourceDto { + /** The user that has created the app. */ + readonly createdBy?: string | undefined; + /** The user that has updated the app. */ + readonly lastModifiedBy?: string | undefined; + /** The ID of the app. */ + readonly id: string; + /** The name of the app. */ + readonly name: string; + /** The optional label of the app. */ + readonly label?: string | undefined; + /** The optional description of the app. */ + readonly description?: string | undefined; + /** The version of the app. */ + readonly version: number; + /** The timestamp when the app has been created. */ + readonly created: DateTime; + /** The timestamp when the app has been modified last. */ + readonly lastModified: DateTime; + /** The ID of the team. */ + readonly teamId?: string | undefined; + /** The permission level of the user. */ + readonly permissions: string[]; + /** Indicates if the user can access the api. */ + readonly canAccessApi: boolean; + /** Indicates if the user can access at least one content. */ + readonly canAccessContent: boolean; + /** The role name of the user. */ + readonly roleName?: string | undefined; + /** The properties from the role. */ + readonly roleProperties: { [key: string]: any; }; +} + +export class CreateAppDto implements ICreateAppDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The name of the app. */ + readonly name!: string; + /** Initialize the app with the inbuilt template. */ + readonly template?: string | undefined; + + constructor(data?: ICreateAppDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).name = _data["name"]; + (this).template = _data["template"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): CreateAppDto { + const result = new CreateAppDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["name"] = this.name; + data["template"] = this.template; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface ICreateAppDto { + /** The name of the app. */ + readonly name: string; + /** Initialize the app with the inbuilt template. */ + readonly template?: string | undefined; +} + +export class UpdateAppDto implements IUpdateAppDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The optional label of your app. */ + readonly label?: string | undefined; + /** The optional description of your app. */ + readonly description?: string | undefined; + + constructor(data?: IUpdateAppDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).label = _data["label"]; + (this).description = _data["description"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): UpdateAppDto { + const result = new UpdateAppDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["label"] = this.label; + data["description"] = this.description; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IUpdateAppDto { + /** The optional label of your app. */ + readonly label?: string | undefined; + /** The optional description of your app. */ + readonly description?: string | undefined; +} + +export class TransferToTeamDto implements ITransferToTeamDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The ID of the team. */ + readonly teamId?: string | undefined; + + constructor(data?: ITransferToTeamDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).teamId = _data["teamId"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): TransferToTeamDto { + const result = new TransferToTeamDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["teamId"] = this.teamId; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface ITransferToTeamDto { + /** The ID of the team. */ + readonly teamId?: string | undefined; +} + +export class AppSettingsDto extends ResourceDto implements IAppSettingsDto { + /** The configured app patterns. */ + readonly patterns!: PatternDto[]; + /** The configured UI editors. */ + readonly editors!: EditorDto[]; + /** Hide the scheduler for content items. */ + readonly hideScheduler!: boolean; + /** Hide the datetime mode button. */ + readonly hideDateTimeModeButton!: boolean; + /** The version of the app. */ + readonly version!: number; + + get canUpdate() { + return this.compute('canUpdate', () => hasAnyLink(this._links, 'update')); + } + + constructor(data?: IAppSettingsDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + if (Array.isArray(_data["patterns"])) { + (this).patterns = [] as any; + for (let item of _data["patterns"]) + (this).patterns!.push(PatternDto.fromJSON(item)); + } + if (Array.isArray(_data["editors"])) { + (this).editors = [] as any; + for (let item of _data["editors"]) + (this).editors!.push(EditorDto.fromJSON(item)); + } + (this).hideScheduler = _data["hideScheduler"]; + (this).hideDateTimeModeButton = _data["hideDateTimeModeButton"]; + (this).version = _data["version"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): AppSettingsDto { + const result = new AppSettingsDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (Array.isArray(this.patterns)) { + data["patterns"] = []; + for (let item of this.patterns) + data["patterns"].push(item.toJSON()); + } + if (Array.isArray(this.editors)) { + data["editors"] = []; + for (let item of this.editors) + data["editors"].push(item.toJSON()); + } + data["hideScheduler"] = this.hideScheduler; + data["hideDateTimeModeButton"] = this.hideDateTimeModeButton; + data["version"] = this.version; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IAppSettingsDto extends IResourceDto { + /** The configured app patterns. */ + readonly patterns: PatternDto[]; + /** The configured UI editors. */ + readonly editors: EditorDto[]; + /** Hide the scheduler for content items. */ + readonly hideScheduler: boolean; + /** Hide the datetime mode button. */ + readonly hideDateTimeModeButton: boolean; + /** The version of the app. */ + readonly version: number; +} + +export class PatternDto implements IPatternDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The name of the suggestion. */ + readonly name!: string; + /** The regex pattern. */ + readonly regex!: string; + /** The regex message. */ + readonly message?: string | undefined; + + constructor(data?: IPatternDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).name = _data["name"]; + (this).regex = _data["regex"]; + (this).message = _data["message"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): PatternDto { + const result = new PatternDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["name"] = this.name; + data["regex"] = this.regex; + data["message"] = this.message; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IPatternDto { + /** The name of the suggestion. */ + readonly name: string; + /** The regex pattern. */ + readonly regex: string; + /** The regex message. */ + readonly message?: string | undefined; +} + +export class EditorDto implements IEditorDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The name of the editor. */ + readonly name!: string; + /** The url to the editor. */ + readonly url!: string; + + constructor(data?: IEditorDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).name = _data["name"]; + (this).url = _data["url"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): EditorDto { + const result = new EditorDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["name"] = this.name; + data["url"] = this.url; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IEditorDto { + /** The name of the editor. */ + readonly name: string; + /** The url to the editor. */ + readonly url: string; +} + +export class UpdateAppSettingsDto implements IUpdateAppSettingsDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The configured app patterns. */ + readonly patterns!: PatternDto[]; + /** The configured UI editors. */ + readonly editors!: EditorDto[]; + /** Hide the scheduler for content items. */ + readonly hideScheduler?: boolean; + /** Hide the datetime mode button. */ + readonly hideDateTimeModeButton?: boolean; + + constructor(data?: IUpdateAppSettingsDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + if (Array.isArray(_data["patterns"])) { + (this).patterns = [] as any; + for (let item of _data["patterns"]) + (this).patterns!.push(PatternDto.fromJSON(item)); + } + if (Array.isArray(_data["editors"])) { + (this).editors = [] as any; + for (let item of _data["editors"]) + (this).editors!.push(EditorDto.fromJSON(item)); + } + (this).hideScheduler = _data["hideScheduler"]; + (this).hideDateTimeModeButton = _data["hideDateTimeModeButton"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): UpdateAppSettingsDto { + const result = new UpdateAppSettingsDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (Array.isArray(this.patterns)) { + data["patterns"] = []; + for (let item of this.patterns) + data["patterns"].push(item.toJSON()); + } + if (Array.isArray(this.editors)) { + data["editors"] = []; + for (let item of this.editors) + data["editors"].push(item.toJSON()); + } + data["hideScheduler"] = this.hideScheduler; + data["hideDateTimeModeButton"] = this.hideDateTimeModeButton; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IUpdateAppSettingsDto { + /** The configured app patterns. */ + readonly patterns: PatternDto[]; + /** The configured UI editors. */ + readonly editors: EditorDto[]; + /** Hide the scheduler for content items. */ + readonly hideScheduler?: boolean; + /** Hide the datetime mode button. */ + readonly hideDateTimeModeButton?: boolean; +} + +export class WorkflowsDto extends ResourceDto implements IWorkflowsDto { + /** The workflow. */ + readonly items!: WorkflowDto[]; + /** The errros that should be fixed. */ + readonly errors!: string[]; + + get canCreate() { + return this.compute('canCreate', () => hasAnyLink(this._links, 'create')); + } + + constructor(data?: IWorkflowsDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + if (Array.isArray(_data["items"])) { + (this).items = [] as any; + for (let item of _data["items"]) + (this).items!.push(WorkflowDto.fromJSON(item)); + } + if (Array.isArray(_data["errors"])) { + (this).errors = [] as any; + for (let item of _data["errors"]) + (this).errors!.push(item); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): WorkflowsDto { + const result = new WorkflowsDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (Array.isArray(this.items)) { + data["items"] = []; + for (let item of this.items) + data["items"].push(item.toJSON()); + } + if (Array.isArray(this.errors)) { + data["errors"] = []; + for (let item of this.errors) + data["errors"].push(item); + } + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IWorkflowsDto extends IResourceDto { + /** The workflow. */ + readonly items: WorkflowDto[]; + /** The errros that should be fixed. */ + readonly errors: string[]; +} + +export class WorkflowDto extends ResourceDto implements IWorkflowDto { + /** The workflow id. */ + readonly id!: string; + /** The name of the workflow. */ + readonly name?: string | undefined; + /** The workflow steps. */ + readonly steps!: { [key: string]: WorkflowStepDto; }; + /** The schema ids. */ + readonly schemaIds?: string[] | undefined; + /** The initial step. */ + readonly initial!: string; + + get displayName() { + return this.compute('displayName', () => StringHelper.firstNonEmpty(this.name, 'i18n:workflows.notNamed')); + } + + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'delete')); + } + + get canUpdate() { + return this.compute('canUpdate', () => hasAnyLink(this._links, 'update')); + } + + constructor(data?: IWorkflowDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).id = _data["id"]; + (this).name = _data["name"]; + if (_data["steps"]) { + (this).steps = {} as any; + for (let key in _data["steps"]) { + if (_data["steps"].hasOwnProperty(key)) + ((this).steps)![key] = _data["steps"][key] ? WorkflowStepDto.fromJSON(_data["steps"][key]) : new WorkflowStepDto(); + } + } + if (Array.isArray(_data["schemaIds"])) { + (this).schemaIds = [] as any; + for (let item of _data["schemaIds"]) + (this).schemaIds!.push(item); + } + (this).initial = _data["initial"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): WorkflowDto { + const result = new WorkflowDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["id"] = this.id; + data["name"] = this.name; + if (this.steps) { + data["steps"] = {}; + for (let key in this.steps) { + if (this.steps.hasOwnProperty(key)) + (data["steps"])[key] = this.steps[key] ? this.steps[key].toJSON() : undefined; + } + } + if (Array.isArray(this.schemaIds)) { + data["schemaIds"] = []; + for (let item of this.schemaIds) + data["schemaIds"].push(item); + } + data["initial"] = this.initial; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IWorkflowDto extends IResourceDto { + /** The workflow id. */ + readonly id: string; + /** The name of the workflow. */ + readonly name?: string | undefined; + /** The workflow steps. */ + readonly steps: { [key: string]: WorkflowStepDto; }; + /** The schema ids. */ + readonly schemaIds?: string[] | undefined; + /** The initial step. */ + readonly initial: string; +} + +export class WorkflowStepDto implements IWorkflowStepDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The transitions. */ + readonly transitions?: { [key: string]: WorkflowTransitionDto; }; + /** The optional color. */ + readonly color?: string | undefined; + /** True if the content should be validated when moving to this step. */ + readonly validate?: boolean; + /** Indicates if updates should not be allowed. */ + readonly noUpdate?: boolean; + /** Optional expression that must evaluate to true when you want to prevent updates. */ + readonly noUpdateExpression?: string | undefined; + /** Optional list of roles to restrict the updates for users with these roles. */ + readonly noUpdateRoles?: string[] | undefined; + + constructor(data?: IWorkflowStepDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + if (_data["transitions"]) { + (this).transitions = {} as any; + for (let key in _data["transitions"]) { + if (_data["transitions"].hasOwnProperty(key)) + ((this).transitions)![key] = _data["transitions"][key] ? WorkflowTransitionDto.fromJSON(_data["transitions"][key]) : new WorkflowTransitionDto(); + } + } + (this).color = _data["color"]; + (this).validate = _data["validate"]; + (this).noUpdate = _data["noUpdate"]; + (this).noUpdateExpression = _data["noUpdateExpression"]; + if (Array.isArray(_data["noUpdateRoles"])) { + (this).noUpdateRoles = [] as any; + for (let item of _data["noUpdateRoles"]) + (this).noUpdateRoles!.push(item); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): WorkflowStepDto { + const result = new WorkflowStepDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (this.transitions) { + data["transitions"] = {}; + for (let key in this.transitions) { + if (this.transitions.hasOwnProperty(key)) + (data["transitions"])[key] = this.transitions[key] ? this.transitions[key].toJSON() : undefined; + } + } + data["color"] = this.color; + data["validate"] = this.validate; + data["noUpdate"] = this.noUpdate; + data["noUpdateExpression"] = this.noUpdateExpression; + if (Array.isArray(this.noUpdateRoles)) { + data["noUpdateRoles"] = []; + for (let item of this.noUpdateRoles) + data["noUpdateRoles"].push(item); + } + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IWorkflowStepDto { + /** The transitions. */ + readonly transitions?: { [key: string]: WorkflowTransitionDto; }; + /** The optional color. */ + readonly color?: string | undefined; + /** True if the content should be validated when moving to this step. */ + readonly validate?: boolean; + /** Indicates if updates should not be allowed. */ + readonly noUpdate?: boolean; + /** Optional expression that must evaluate to true when you want to prevent updates. */ + readonly noUpdateExpression?: string | undefined; + /** Optional list of roles to restrict the updates for users with these roles. */ + readonly noUpdateRoles?: string[] | undefined; +} + +export class WorkflowTransitionDto implements IWorkflowTransitionDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The optional expression. */ + readonly expression?: string | undefined; + /** The optional restricted role. */ + readonly roles?: string[] | undefined; + + constructor(data?: IWorkflowTransitionDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).expression = _data["expression"]; + if (Array.isArray(_data["roles"])) { + (this).roles = [] as any; + for (let item of _data["roles"]) + (this).roles!.push(item); + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): WorkflowTransitionDto { + const result = new WorkflowTransitionDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["expression"] = this.expression; + if (Array.isArray(this.roles)) { + data["roles"] = []; + for (let item of this.roles) + data["roles"].push(item); + } + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IWorkflowTransitionDto { + /** The optional expression. */ + readonly expression?: string | undefined; + /** The optional restricted role. */ + readonly roles?: string[] | undefined; +} + +export class AddWorkflowDto implements IAddWorkflowDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The name of the workflow. */ + readonly name!: string; + + constructor(data?: IAddWorkflowDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).name = _data["name"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): AddWorkflowDto { + const result = new AddWorkflowDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["name"] = this.name; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IAddWorkflowDto { + /** The name of the workflow. */ + readonly name: string; +} + +export class UpdateWorkflowDto implements IUpdateWorkflowDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The name of the workflow. */ + readonly name?: string | undefined; + /** The workflow steps. */ + readonly steps!: { [key: string]: WorkflowStepDto; }; + /** The schema ids. */ + readonly schemaIds?: string[] | undefined; + /** The initial step. */ + readonly initial!: string; + + constructor(data?: IUpdateWorkflowDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).name = _data["name"]; + if (_data["steps"]) { + (this).steps = {} as any; + for (let key in _data["steps"]) { + if (_data["steps"].hasOwnProperty(key)) + ((this).steps)![key] = _data["steps"][key] ? WorkflowStepDto.fromJSON(_data["steps"][key]) : new WorkflowStepDto(); + } + } + if (Array.isArray(_data["schemaIds"])) { + (this).schemaIds = [] as any; + for (let item of _data["schemaIds"]) + (this).schemaIds!.push(item); + } + (this).initial = _data["initial"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): UpdateWorkflowDto { + const result = new UpdateWorkflowDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["name"] = this.name; + if (this.steps) { + data["steps"] = {}; + for (let key in this.steps) { + if (this.steps.hasOwnProperty(key)) + (data["steps"])[key] = this.steps[key] ? this.steps[key].toJSON() : undefined; + } + } + if (Array.isArray(this.schemaIds)) { + data["schemaIds"] = []; + for (let item of this.schemaIds) + data["schemaIds"].push(item); + } + data["initial"] = this.initial; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IUpdateWorkflowDto { + /** The name of the workflow. */ + readonly name?: string | undefined; + /** The workflow steps. */ + readonly steps: { [key: string]: WorkflowStepDto; }; + /** The schema ids. */ + readonly schemaIds?: string[] | undefined; + /** The initial step. */ + readonly initial: string; +} + +export class DynamicCreateRuleDto implements IDynamicCreateRuleDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** The trigger properties. */ + readonly trigger!: RuleTriggerDto; + /** The action properties. */ + readonly action!: { [key: string]: any; }; + + constructor(data?: IDynamicCreateRuleDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).trigger = _data["trigger"] ? RuleTriggerDto.fromJSON(_data["trigger"]) : undefined; + if (_data["action"]) { + (this).action = {} as any; + for (let key in _data["action"]) { + if (_data["action"].hasOwnProperty(key)) + ((this).action)![key] = _data["action"][key]; + } + } + this.cleanup(this); + return this; + } + + static fromJSON(data: any): DynamicCreateRuleDto { + const result = new DynamicCreateRuleDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["trigger"] = this.trigger ? this.trigger.toJSON() : undefined; + if (this.action) { + data["action"] = {}; + for (let key in this.action) { + if (this.action.hasOwnProperty(key)) + (data["action"])[key] = (this.action)[key]; + } + } + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IDynamicCreateRuleDto { + /** The trigger properties. */ + readonly trigger: RuleTriggerDto; + /** The action properties. */ + readonly action: { [key: string]: any; }; +} + +export class DynamicRulesDto extends ResourceDto implements IDynamicRulesDto { + /** The rules. */ + readonly items!: DynamicRuleDto[]; + /** The ID of the rule that is currently rerunning. */ + readonly runningRuleId?: string | undefined; + + get canCreate() { + return this.compute('canCreate', () => hasAnyLink(this._links, 'create')); + } + + get canReadEvents() { + return this.compute('canReadEvents', () => hasAnyLink(this._links, 'events')); + } + + get canCancelRun() { + return this.compute('canCancelRun', () => hasAnyLink(this._links, 'run/cancel')); + } + + constructor(data?: IDynamicRulesDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + if (Array.isArray(_data["items"])) { + (this).items = [] as any; + for (let item of _data["items"]) + (this).items!.push(DynamicRuleDto.fromJSON(item)); + } + (this).runningRuleId = _data["runningRuleId"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): DynamicRulesDto { + const result = new DynamicRulesDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (Array.isArray(this.items)) { + data["items"] = []; + for (let item of this.items) + data["items"].push(item.toJSON()); + } + data["runningRuleId"] = this.runningRuleId; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IDynamicRulesDto extends IResourceDto { + /** The rules. */ + readonly items: DynamicRuleDto[]; + /** The ID of the rule that is currently rerunning. */ + readonly runningRuleId?: string | undefined; +} + +export class DynamicRuleDto extends ResourceDto implements IDynamicRuleDto { + /** The ID of the rule. */ + readonly id!: string; + /** The user that has created the rule. */ + readonly createdBy!: string; + /** The user that has updated the rule. */ + readonly lastModifiedBy!: string; + /** The date and time when the rule has been created. */ + readonly created!: DateTime; + /** The date and time when the rule has been modified last. */ + readonly lastModified!: DateTime; + /** The version of the rule. */ + readonly version!: number; + /** Determines if the rule is enabled. */ + readonly isEnabled!: boolean; + /** Optional rule name. */ + readonly name?: string | undefined; + /** The trigger properties. */ + readonly trigger!: RuleTriggerDto; + /** The action properties. */ + readonly action!: { [key: string]: any; }; + /** The number of completed executions. */ + readonly numSucceeded!: number; + /** The number of failed executions. */ + readonly numFailed!: number; + /** The date and time when the rule was executed the last time. */ + readonly lastExecuted?: DateTime | undefined; + + get canDelete() { + return this.compute('canDelete', () => hasAnyLink(this._links, 'delete')); + } + + get canDisable() { + return this.compute('canDisable', () => hasAnyLink(this._links, 'disable')); + } + + get canEnable() { + return this.compute('canEnable', () => hasAnyLink(this._links, 'enable')); + } + + get canReadLogs() { + return this.compute('canReadLogs', () => hasAnyLink(this._links, 'logs')); + } + + get canRun() { + return this.compute('canRun', () => hasAnyLink(this._links, 'run')); + } + + get canRunFromSnapshots() { + return this.compute('canRunFromSnapshots', () => hasAnyLink(this._links, 'run/snapshots')); + } + + get canTrigger() { + return this.compute('canTrigger', () => hasAnyLink(this._links, 'trigger')); + } + + get canUpdate() { + return this.compute('canUpdate', () => hasAnyLink(this._links, 'update')); + } + + constructor(data?: IDynamicRuleDto) { + super(data); + } + + init(_data: any) { + super.init(_data); + (this).id = _data["id"]; + (this).createdBy = _data["createdBy"]; + (this).lastModifiedBy = _data["lastModifiedBy"]; + (this).created = _data["created"] ? DateTime.parseISO(_data["created"].toString()) : undefined; + (this).lastModified = _data["lastModified"] ? DateTime.parseISO(_data["lastModified"].toString()) : undefined; + (this).version = _data["version"]; + (this).isEnabled = _data["isEnabled"]; + (this).name = _data["name"]; + (this).trigger = _data["trigger"] ? RuleTriggerDto.fromJSON(_data["trigger"]) : undefined; + if (_data["action"]) { + (this).action = {} as any; + for (let key in _data["action"]) { + if (_data["action"].hasOwnProperty(key)) + ((this).action)![key] = _data["action"][key]; + } + } + (this).numSucceeded = _data["numSucceeded"]; + (this).numFailed = _data["numFailed"]; + (this).lastExecuted = _data["lastExecuted"] ? DateTime.parseISO(_data["lastExecuted"].toString()) : undefined; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): DynamicRuleDto { + const result = new DynamicRuleDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["id"] = this.id; + data["createdBy"] = this.createdBy; + data["lastModifiedBy"] = this.lastModifiedBy; + data["created"] = this.created ? this.created.toISOString() : undefined; + data["lastModified"] = this.lastModified ? this.lastModified.toISOString() : undefined; + data["version"] = this.version; + data["isEnabled"] = this.isEnabled; + data["name"] = this.name; + data["trigger"] = this.trigger ? this.trigger.toJSON() : undefined; + if (this.action) { + data["action"] = {}; + for (let key in this.action) { + if (this.action.hasOwnProperty(key)) + (data["action"])[key] = (this.action)[key]; + } + } + data["numSucceeded"] = this.numSucceeded; + data["numFailed"] = this.numFailed; + data["lastExecuted"] = this.lastExecuted ? this.lastExecuted.toISOString() : undefined; + super.toJSON(data); + this.cleanup(data); + return data; + } +} + +export interface IDynamicRuleDto extends IResourceDto { + /** The ID of the rule. */ + readonly id: string; + /** The user that has created the rule. */ + readonly createdBy: string; + /** The user that has updated the rule. */ + readonly lastModifiedBy: string; + /** The date and time when the rule has been created. */ + readonly created: DateTime; + /** The date and time when the rule has been modified last. */ + readonly lastModified: DateTime; + /** The version of the rule. */ + readonly version: number; + /** Determines if the rule is enabled. */ + readonly isEnabled: boolean; + /** Optional rule name. */ + readonly name?: string | undefined; + /** The trigger properties. */ + readonly trigger: RuleTriggerDto; + /** The action properties. */ + readonly action: { [key: string]: any; }; + /** The number of completed executions. */ + readonly numSucceeded: number; + /** The number of failed executions. */ + readonly numFailed: number; + /** The date and time when the rule was executed the last time. */ + readonly lastExecuted?: DateTime | undefined; +} + +export class DynamicUpdateRuleDto implements IDynamicUpdateRuleDto { + /** Uses the cache values because the actual object is frozen. */ + private readonly cachedValues: { [key: string]: any } = {}; + /** Optional rule name. */ + readonly name?: string | undefined; + /** The trigger properties. */ + readonly trigger?: RuleTriggerDto | undefined; + /** The action properties. */ + readonly action?: { [key: string]: any; } | undefined; + /** Enable or disable the rule. */ + readonly isEnabled?: boolean | undefined; + + constructor(data?: IDynamicUpdateRuleDto) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data: any) { + (this).name = _data["name"]; + (this).trigger = _data["trigger"] ? RuleTriggerDto.fromJSON(_data["trigger"]) : undefined; + if (_data["action"]) { + (this).action = {} as any; + for (let key in _data["action"]) { + if (_data["action"].hasOwnProperty(key)) + ((this).action)![key] = _data["action"][key]; + } + } + (this).isEnabled = _data["isEnabled"]; + this.cleanup(this); + return this; + } + + static fromJSON(data: any): DynamicUpdateRuleDto { + const result = new DynamicUpdateRuleDto().init(data); + result.cleanup(this); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["name"] = this.name; + data["trigger"] = this.trigger ? this.trigger.toJSON() : undefined; + if (this.action) { + data["action"] = {}; + for (let key in this.action) { + if (this.action.hasOwnProperty(key)) + (data["action"])[key] = (this.action)[key]; + } + } + data["isEnabled"] = this.isEnabled; + this.cleanup(data); + return data; + } + + protected cleanup(target: any) { + for (var property in target) { + if (target.hasOwnProperty(property)) { + const value = target[property]; + if (value === undefined) { + delete target[property]; + } + } + } + } + + protected compute(key: string, action: () => T): T { + if (!this.cachedValues.hasOwnProperty(key)) { + const value = action(); + this.cachedValues[key] = value; + return value; + } else { + return this.cachedValues[key] as any; + } + } +} + +export interface IDynamicUpdateRuleDto { + /** Optional rule name. */ + readonly name?: string | undefined; + /** The trigger properties. */ + readonly trigger?: RuleTriggerDto | undefined; + /** The action properties. */ + readonly action?: { [key: string]: any; } | undefined; + /** Enable or disable the rule. */ + readonly isEnabled?: boolean | undefined; +} + +export interface FileParameter { + data: any; + fileName: string; +} + +export interface FileResponse { + data: Blob; + status: number; + fileName?: string; + headers?: { [name: string]: any }; +} + +/* eslint-disable sort-imports */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars + + + + + + + + + + + + + +// +// FIELD TYPES +// \ No newline at end of file diff --git a/frontend/src/app/shared/model/index.ts b/frontend/src/app/shared/model/index.ts new file mode 100644 index 000000000..9ef63dd3c --- /dev/null +++ b/frontend/src/app/shared/model/index.ts @@ -0,0 +1,9 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +export * from './generated'; +export * from './schemas'; \ No newline at end of file diff --git a/frontend/src/app/shared/model/schemas.ts b/frontend/src/app/shared/model/schemas.ts new file mode 100644 index 000000000..b3d6fc895 --- /dev/null +++ b/frontend/src/app/shared/model/schemas.ts @@ -0,0 +1,287 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { ArrayFieldPropertiesDto, AssetsFieldPropertiesDto, BooleanFieldPropertiesDto, ComponentFieldPropertiesDto, ComponentsFieldPropertiesDto, DateTimeFieldPropertiesDto, FieldDto, FieldPropertiesDto, FieldRuleAction, GeolocationFieldPropertiesDto, JsonFieldPropertiesDto, NumberFieldPropertiesDto, ReferencesFieldPropertiesDto, RichTextFieldPropertiesDto, StringFieldPropertiesDto, TagsFieldPropertiesDto, UIFieldPropertiesDto } from './generated'; + +export type FieldType = + 'Array' | + 'Assets' | + 'Boolean' | + 'Component' | + 'Components' | + 'DateTime' | + 'Json' | + 'Geolocation' | + 'Number' | + 'References' | + 'RichText' | + 'String' | + 'Tags' | + 'UI'; + +export const fieldTypes: ReadonlyArray<{ type: FieldType; description: string }> = [ + { + type: 'String', + description: 'i18n:schemas.fieldTypes.string.description', + }, { + type: 'Assets', + description: 'i18n:schemas.fieldTypes.assets.description', + }, { + type: 'Boolean', + description: 'i18n:schemas.fieldTypes.boolean.description', + }, { + type: 'Component', + description: 'i18n:schemas.fieldTypes.component.description', + }, { + type: 'Components', + description: 'i18n:schemas.fieldTypes.components.description', + }, { + type: 'DateTime', + description: 'i18n:schemas.fieldTypes.dateTime.description', + }, { + type: 'Geolocation', + description: 'i18n:schemas.fieldTypes.geolocation.description', + }, { + type: 'Json', + description: 'i18n:schemas.fieldTypes.json.description', + }, { + type: 'Number', + description: 'i18n:schemas.fieldTypes.number.description', + }, { + type: 'References', + description: 'i18n:schemas.fieldTypes.references.description', + }, { + type: 'RichText', + description: 'i18n:schemas.fieldTypes.richText.description', + }, { + type: 'Tags', + description: 'i18n:schemas.fieldTypes.tags.description', + }, { + type: 'Array', + description: 'i18n:schemas.fieldTypes.array.description', + }, { + type: 'UI', + description: 'i18n:schemas.fieldTypes.ui.description', + }, +]; + +export const fieldInvariant = 'iv'; + +export function createProperties(fieldType: FieldType, values?: any): FieldPropertiesDto { + let properties: FieldPropertiesDto; + + switch (fieldType) { + case 'Array': + properties = new ArrayFieldPropertiesDto(values); + break; + case 'Assets': + properties = new AssetsFieldPropertiesDto(values); + break; + case 'Boolean': + properties = new BooleanFieldPropertiesDto(values); + break; + case 'Component': + properties = new ComponentFieldPropertiesDto(values); + break; + case 'Components': + properties = new ComponentsFieldPropertiesDto(values); + break; + case 'DateTime': + properties = new DateTimeFieldPropertiesDto(values); + break; + case 'Geolocation': + properties = new GeolocationFieldPropertiesDto(values); + break; + case 'Json': + properties = new JsonFieldPropertiesDto(values); + break; + case 'Number': + properties = new NumberFieldPropertiesDto(values); + break; + case 'References': + properties = new ReferencesFieldPropertiesDto(values); + break; + case 'RichText': + properties = new RichTextFieldPropertiesDto(values); + break; + case 'String': + properties = new StringFieldPropertiesDto(values); + break; + case 'Tags': + properties = new TagsFieldPropertiesDto(values); + break; + case 'UI': + properties = new UIFieldPropertiesDto(values); + break; + default: + throw new Error(`Unknown field type ${fieldType}.`); + } + + return properties; +} + +export interface FieldPropertiesVisitor { + visitArray(properties: ArrayFieldPropertiesDto): T; + + visitAssets(properties: AssetsFieldPropertiesDto): T; + + visitBoolean(properties: BooleanFieldPropertiesDto): T; + + visitComponent(properties: ComponentFieldPropertiesDto): T; + + visitComponents(properties: ComponentsFieldPropertiesDto): T; + + visitDateTime(properties: DateTimeFieldPropertiesDto): T; + + visitGeolocation(properties: GeolocationFieldPropertiesDto): T; + + visitJson(properties: JsonFieldPropertiesDto): T; + + visitNumber(properties: NumberFieldPropertiesDto): T; + + visitReferences(properties: ReferencesFieldPropertiesDto): T; + + visitRichText(properties: RichTextFieldPropertiesDto): T; + + visitString(properties: StringFieldPropertiesDto): T; + + visitTags(properties: TagsFieldPropertiesDto): T; + + visitUI(properties: UIFieldPropertiesDto): T; +} + +export const META_FIELDS = { + empty: { + name: '', + label: '', + title: '', + }, + id: { + name: 'id', + label: 'i18n:schemas.tableHeaders.id', + title: 'i18n:schemas.tableHeaders.id_title', + }, + created: { + name: 'created', + label: 'i18n:schemas.tableHeaders.created', + title: 'i18n:schemas.tableHeaders.created_title', + }, + createdByAvatar: { + name: 'createdBy.avatar', + label: 'i18n:schemas.tableHeaders.createdByShort', + title: 'i18n:schemas.tableHeaders.createdByShort_title', + }, + createdByName: { + name: 'createdBy.name', + label: 'i18n:schemas.tableHeaders.createdBy', + title: 'i18n:schemas.tableHeaders.createdBy_title', + }, + lastModified: { + name: 'lastModified', + label: 'i18n:schemas.tableHeaders.lastModified', + title: 'i18n:schemas.tableHeaders.lastModified_title', + }, + lastModifiedByAvatar: { + name: 'lastModifiedBy.avatar', + label: 'i18n:schemas.tableHeaders.lastModifiedByShort', + title: 'i18n:schemas.tableHeaders.lastModifiedByShort_title', + }, + lastModifiedByName: { + name: 'lastModifiedBy.name', + label: 'i18n:schemas.tableHeaders.lastModifiedBy', + title: 'i18n:schemas.tableHeaders.lastModifiedBy_title', + }, + status: { + name: 'status', + label: 'i18n:schemas.tableHeaders.status', + title: 'i18n:schemas.tableHeaders.status_title', + }, + statusColor: { + name: 'status.color', + label: 'i18n:schemas.tableHeaders.status', + title: 'i18n:schemas.tableHeaders.status_title', + }, + statusNext: { + name: 'status.next', + label: 'i18n:schemas.tableHeaders.nextStatus', + title: 'i18n:schemas.tableHeaders.nextStatus_title', + }, + version: { + name: 'version', + label: 'i18n:schemas.tableHeaders.version', + title: 'i18n:schemas.tableHeaders.version_title', + }, + translationStatus: { + name: 'translationStatus', + label: 'i18n:schemas.tableHeaders.translationStatus', + title: 'i18n:schemas.tableHeaders.translationStatus_title', + }, + translationStatusAverage: { + name: 'translationStatusAverage', + label: 'i18n:schemas.tableHeaders.translationStatusAverage', + title: 'i18n:schemas.tableHeaders.translationStatusAverage_title', + }, +}; + +export const FIELD_RULE_ACTIONS: ReadonlyArray = [ + 'Disable', + 'Hide', + 'Require', +]; + +export type TableField = Readonly<{ + // The name of the table field. + name: string; + + // The label for the table header. + label: string; + + // The title. + title?: string; + + // The reference to the root field. + rootField?: FieldDto; +}>; + +export function getTableFields(fields: ReadonlyArray) { + const result: string[] = []; + + for (const field of fields) { + if (field.name?.startsWith('data.')) { + result.push(field.name); + } + } + + result.sort(); + return result; +} + +export function tableField(rootField: FieldDto): TableField { + const label = rootField.displayName; + + return { name: `data.${rootField.name}`, label, rootField }; +} + +export function tableFields(names: ReadonlyArray, fields: ReadonlyArray): TableField[] { + const result: TableField[] = []; + + for (const name of names) { + const metaField = Object.values(META_FIELDS).find(x => x.name === name); + + if (metaField) { + result.push(metaField); + } else { + const field = fields.find(x => x.name === name); + + if (field) { + result.push(field); + } + } + } + + return result; +} \ No newline at end of file diff --git a/frontend/src/app/shared/services/app-languages.service.spec.ts b/frontend/src/app/shared/services/app-languages.service.spec.ts index b2d044ada..38359c773 100644 --- a/frontend/src/app/shared/services/app-languages.service.spec.ts +++ b/frontend/src/app/shared/services/app-languages.service.spec.ts @@ -8,10 +8,11 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { ApiUrlConfig, AppLanguageDto, AppLanguagesDto, AppLanguagesPayload, AppLanguagesService, Resource, ResourceLinks, Version } from '@app/shared/internal'; +import { ApiUrlConfig, AppLanguageDto, AppLanguagesDto, AppLanguagesService, Resource, Versioned, VersionTag } from '@app/shared/internal'; +import { AddLanguageDto, ResourceLinkDto, UpdateLanguageDto } from '../model'; describe('AppLanguagesService', () => { - const version = new Version('1'); + const version = new VersionTag('1'); beforeEach(() => { TestBed.configureTestingModule({ @@ -31,8 +32,7 @@ describe('AppLanguagesService', () => { it('should make get request to get app languages', inject([AppLanguagesService, HttpTestingController], (appLanguagesService: AppLanguagesService, httpMock: HttpTestingController) => { - let languages: AppLanguagesDto; - + let languages: Versioned; appLanguagesService.getLanguages('my-app').subscribe(result => { languages = result; }); @@ -48,15 +48,14 @@ describe('AppLanguagesService', () => { }, }); - expect(languages!).toEqual({ payload: createLanguages('en', 'de', 'it'), version: new Version('2') }); + expect(languages!).toEqual({ payload: createLanguages('en', 'de', 'it'), version: new VersionTag('2') }); })); it('should make post request to add language', inject([AppLanguagesService, HttpTestingController], (appLanguagesService: AppLanguagesService, httpMock: HttpTestingController) => { - const dto = { language: 'de' }; - - let languages: AppLanguagesDto; + const dto = new AddLanguageDto({ language: 'de' }); + let languages: Versioned; appLanguagesService.postLanguage('my-app', dto, version).subscribe(result => { languages = result; }); @@ -72,12 +71,12 @@ describe('AppLanguagesService', () => { }, }); - expect(languages!).toEqual({ payload: createLanguages('en', 'de', 'it'), version: new Version('2') }); + expect(languages!).toEqual({ payload: createLanguages('en', 'de', 'it'), version: new VersionTag('2') }); })); it('should make put request to make master language', inject([AppLanguagesService, HttpTestingController], (appLanguagesService: AppLanguagesService, httpMock: HttpTestingController) => { - const dto = { isMaster: true }; + const dto = new UpdateLanguageDto({ isMaster: true }); const resource: Resource = { _links: { @@ -85,8 +84,7 @@ describe('AppLanguagesService', () => { }, }; - let languages: AppLanguagesDto; - + let languages: Versioned; appLanguagesService.putLanguage('my-app', resource, dto, version).subscribe(result => { languages = result; }); @@ -102,7 +100,7 @@ describe('AppLanguagesService', () => { }, }); - expect(languages!).toEqual({ payload: createLanguages('en', 'de', 'it'), version: new Version('2') }); + expect(languages!).toEqual({ payload: createLanguages('en', 'de', 'it'), version: new VersionTag('2') }); })); it('should make delete request to remove language', @@ -113,8 +111,7 @@ describe('AppLanguagesService', () => { }, }; - let languages: AppLanguagesDto; - + let languages: Versioned; appLanguagesService.deleteLanguage('my-app', resource, version).subscribe(result => { languages = result; }); @@ -130,7 +127,7 @@ describe('AppLanguagesService', () => { }, }); - expect(languages!).toEqual({ payload: createLanguages('en', 'de', 'it'), version: new Version('2') }); + expect(languages!).toEqual({ payload: createLanguages('en', 'de', 'it'), version: new VersionTag('2') }); })); function languagesResponse(...codes: string[]) { @@ -152,16 +149,20 @@ describe('AppLanguagesService', () => { } }); -export function createLanguages(...codes: ReadonlyArray): AppLanguagesPayload { - return { - items: codes.map((code, i) => createLanguage(code, codes, i)), - canCreate: true, - }; -} -function createLanguage(code: string, codes: ReadonlyArray, i: number) { - const links: ResourceLinks = { - update: { method: 'PUT', href: `/languages/${code}` }, - }; - - return new AppLanguageDto(links, code, code, i === 0, i % 2 === 1, codes.removed(code)); +export function createLanguages(...codes: Array): AppLanguagesDto { + return new AppLanguagesDto({ + items: codes.map((code, i) => new AppLanguageDto({ + iso2Code: code, + englishName: code, + isMaster: i === 0, + isOptional: i % 2 === 1, + fallback: codes.removed(code), + _links: { + update: new ResourceLinkDto({ method: 'PUT', href: `/languages/${code}` }), + }, + })), + _links: { + create: new ResourceLinkDto({ method: 'POST', href: '/languages' }), + }, + }); } diff --git a/frontend/src/app/shared/services/app-languages.service.ts b/frontend/src/app/shared/services/app-languages.service.ts index 697b58b91..4f0167fc1 100644 --- a/frontend/src/app/shared/services/app-languages.service.ts +++ b/frontend/src/app/shared/services/app-languages.service.ts @@ -8,53 +8,8 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { ApiUrlConfig, hasAnyLink, HTTP, mapVersioned, pretifyError, Resource, ResourceLinks, Version, Versioned } from '@app/framework'; - -export class AppLanguageDto { - public readonly _links: ResourceLinks; - - public readonly canDelete: boolean; - public readonly canUpdate: boolean; - - constructor(links: ResourceLinks, - public readonly iso2Code: string, - public readonly englishName: string, - public readonly isMaster: boolean, - public readonly isOptional: boolean, - public readonly fallback: ReadonlyArray, - ) { - this._links = links; - - this.canDelete = hasAnyLink(links, 'delete'); - this.canUpdate = hasAnyLink(links, 'update'); - } -} - -export type AppLanguagesDto = Versioned; - -export type AppLanguagesPayload = Readonly<{ - // The app languages. - items: ReadonlyArray; - - // The if the user can create a new language. - canCreate?: boolean; -}>; - -export type AddAppLanguageDto = Readonly<{ - // The language code to add. - language: string; -}>; - -export type UpdateAppLanguageDto = Readonly<{ - // Indicates if the language is the master language. - isMaster?: boolean; - - // Indicates if the langauge is optional (cannot be master language). - isOptional?: boolean; - - // The fallback language codes. - falback?: ReadonlyArray; -}>; +import { ApiUrlConfig, HTTP, mapVersioned, pretifyError, Resource, Versioned, VersionOrTag } from '@app/framework'; +import { AddLanguageDto, AppLanguagesDto, UpdateLanguageDto } from '../model'; @Injectable({ providedIn: 'root', @@ -66,65 +21,47 @@ export class AppLanguagesService { ) { } - public getLanguages(appName: string): Observable { + public getLanguages(appName: string): Observable> { const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages`); return HTTP.getVersioned(this.http, url).pipe( mapVersioned(({ body }) => { - return parseLanguages(body); + return AppLanguagesDto.fromJSON(body); }), pretifyError('i18n:languages.loadFailed')); } - public postLanguage(appName: string, dto: AddAppLanguageDto, version: Version): Observable { + public postLanguage(appName: string, dto: AddLanguageDto, version: VersionOrTag): Observable> { const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages`); - return HTTP.postVersioned(this.http, url, dto, version).pipe( + return HTTP.postVersioned(this.http, url, dto.toJSON(), version).pipe( mapVersioned(({ body }) => { - return parseLanguages(body); + return AppLanguagesDto.fromJSON(body); }), pretifyError('i18n:languages.addFailed')); } - public putLanguage(appName: string, resource: Resource, dto: UpdateAppLanguageDto, version: Version): Observable { + public putLanguage(appName: string, resource: Resource, dto: UpdateLanguageDto, version: VersionOrTag): Observable> { const link = resource._links['update']; const url = this.apiUrl.buildUrl(link.href); - return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe( + return HTTP.requestVersioned(this.http, link.method, url, version, dto.toJSON()).pipe( mapVersioned(({ body }) => { - return parseLanguages(body); + return AppLanguagesDto.fromJSON(body); }), pretifyError('i18n:languages.updateFailed')); } - public deleteLanguage(appName: string, resource: Resource, version: Version): Observable { + public deleteLanguage(appName: string, resource: Resource, version: VersionOrTag): Observable> { const link = resource._links['delete']; const url = this.apiUrl.buildUrl(link.href); return HTTP.requestVersioned(this.http, link.method, url, version).pipe( mapVersioned(({ body }) => { - return parseLanguages(body); + return AppLanguagesDto.fromJSON(body); }), pretifyError('i18n:languages.deleteFailed')); } } - -function parseLanguages(response: { items: any[] } & Resource): AppLanguagesPayload { - const { items: list, _links } = response; - const items = list.map(parseLanguage); - - const canCreate = hasAnyLink(_links, 'create'); - - return { items, canCreate }; -} - -function parseLanguage(response: any) { - return new AppLanguageDto(response._links, - response.iso2Code, - response.englishName, - response.isMaster, - response.isOptional, - response.fallback || []); -} diff --git a/frontend/src/app/shared/services/apps.service.spec.ts b/frontend/src/app/shared/services/apps.service.spec.ts index ae710ab98..68a4e5db2 100644 --- a/frontend/src/app/shared/services/apps.service.spec.ts +++ b/frontend/src/app/shared/services/apps.service.spec.ts @@ -8,10 +8,11 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { ApiUrlConfig, AppDto, AppSettingsDto, AppsService, AssetScriptsDto, AssetScriptsPayload, DateTime, EditorDto, ErrorDto, PatternDto, Resource, ResourceLinks, Version } from '@app/shared/internal'; +import { ApiUrlConfig, AppDto, AppSettingsDto, AppsService, AssetScriptsDto, DateTime, EditorDto, ErrorDto, PatternDto, Resource, Versioned, VersionTag } from '@app/shared/internal'; +import { CreateAppDto, ResourceLinkDto, TransferToTeamDto, UpdateAppDto, UpdateAppSettingsDto, UpdateAssetScriptsDto } from '../model'; describe('AppsService', () => { - const version = new Version('1'); + const version = new VersionTag('1'); beforeEach(() => { TestBed.configureTestingModule({ @@ -32,7 +33,6 @@ describe('AppsService', () => { it('should make get request to get apps', inject([AppsService, HttpTestingController], (appsService: AppsService, httpMock: HttpTestingController) => { let apps: ReadonlyArray; - appsService.getApps().subscribe(result => { apps = result; }); @@ -54,7 +54,6 @@ describe('AppsService', () => { it('should make get request to get team apps', inject([AppsService, HttpTestingController], (appsService: AppsService, httpMock: HttpTestingController) => { let apps: ReadonlyArray; - appsService.getTeamApps('my-team').subscribe(result => { apps = result; }); @@ -75,7 +74,6 @@ describe('AppsService', () => { it('should make get request to get app', inject([AppsService, HttpTestingController], (appsService: AppsService, httpMock: HttpTestingController) => { let app: AppDto; - appsService.getApp('my-app').subscribe(result => { app = result; }); @@ -93,7 +91,6 @@ describe('AppsService', () => { it('should make get request to get app settings', inject([AppsService, HttpTestingController], (appsService: AppsService, httpMock: HttpTestingController) => { let settings: AppSettingsDto; - appsService.getSettings('my-app').subscribe(result => { settings = result; }); @@ -110,6 +107,8 @@ describe('AppsService', () => { it('should make put request to update app settings', inject([AppsService, HttpTestingController], (appsService: AppsService, httpMock: HttpTestingController) => { + const dto = new UpdateAppSettingsDto({ editors: [], patterns: [] }); + const resource: Resource = { _links: { update: { method: 'PUT', href: '/api/apps/my-app/settings' }, @@ -117,8 +116,7 @@ describe('AppsService', () => { }; let settings: AppSettingsDto; - - appsService.putSettings('my-app', resource, {} as any, version).subscribe(result => { + appsService.putSettings('my-app', resource, dto, version).subscribe(result => { settings = result; }); @@ -134,10 +132,9 @@ describe('AppsService', () => { it('should make get request to get asset scripts', inject([AppsService, HttpTestingController], (appsService: AppsService, httpMock: HttpTestingController) => { - let settings: AssetScriptsDto; - + let scripts: Versioned; appsService.getAssetScripts('my-app').subscribe(result => { - settings = result; + scripts = result; }); const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets/scripts'); @@ -151,20 +148,21 @@ describe('AppsService', () => { }, }); - expect(settings!).toEqual({ payload: createAssetScripts(12), version: new Version('2') }); + expect(scripts!).toEqual({ payload: createAssetScripts(12), version: new VersionTag('2') }); })); it('should make put request to update asset scripts', inject([AppsService, HttpTestingController], (appsService: AppsService, httpMock: HttpTestingController) => { + const dto = new UpdateAssetScriptsDto({}); + const resource: Resource = { _links: { update: { method: 'PUT', href: '/api/apps/my-app/assets/scripts' }, }, }; - let scripts: AssetScriptsDto; - - appsService.putAssetScripts('my-app', resource, {} as any, version).subscribe(result => { + let scripts: Versioned; + appsService.putAssetScripts('my-app', resource, dto, version).subscribe(result => { scripts = result; }); @@ -179,15 +177,14 @@ describe('AppsService', () => { }, }); - expect(scripts!).toEqual({ payload: createAssetScripts(12), version: new Version('2') }); + expect(scripts!).toEqual({ payload: createAssetScripts(12), version: new VersionTag('2') }); })); it('should make post request to create app', inject([AppsService, HttpTestingController], (appsService: AppsService, httpMock: HttpTestingController) => { - const dto = { name: 'new-app' }; + const dto = new CreateAppDto({ name: 'new-app' }); let app: AppDto; - appsService.postApp(dto).subscribe(result => { app = result; }); @@ -204,6 +201,8 @@ describe('AppsService', () => { it('should make put request to update app', inject([AppsService, HttpTestingController], (appsService: AppsService, httpMock: HttpTestingController) => { + const dto = new UpdateAppDto({}); + const resource: Resource = { _links: { update: { method: 'PUT', href: '/api/apps/my-app' }, @@ -211,8 +210,7 @@ describe('AppsService', () => { }; let app: AppDto; - - appsService.putApp('my-app', resource, { }, version).subscribe(result => { + appsService.putApp('my-app', resource, dto, version).subscribe(result => { app = result; }); @@ -228,6 +226,8 @@ describe('AppsService', () => { it('should make put request to transfer app', inject([AppsService, HttpTestingController], (appsService: AppsService, httpMock: HttpTestingController) => { + const dto = new TransferToTeamDto({ teamId: 'my-team' }); + const resource: Resource = { _links: { transfer: { method: 'PUT', href: '/api/apps/my-app/team' }, @@ -235,8 +235,7 @@ describe('AppsService', () => { }; let app: AppDto; - - appsService.transferApp('my-app', resource, { teamId: 'my-team' }, version).subscribe(result => { + appsService.transferApp('my-app', resource, dto, version).subscribe(result => { app = result; }); @@ -259,7 +258,6 @@ describe('AppsService', () => { }; let app: AppDto; - appsService.postAppImage('my-app', resource, null!, version).subscribe(result => { app = result; }); @@ -283,7 +281,6 @@ describe('AppsService', () => { }; let error: ErrorDto; - appsService.postAppImage('my-app', resource, null!, version).subscribe({ error: e => { error = e; @@ -309,7 +306,6 @@ describe('AppsService', () => { }; let app: AppDto; - appsService.deleteAppImage('my-app', resource, version).subscribe(result => { app = result; }); @@ -365,20 +361,20 @@ describe('AppsService', () => { return { id: `id${id}`, + canAccessApi: id % 2 === 0, + canAccessContent: id % 2 === 0, created: buildDate(id, 10), createdBy: `creator${id}`, + description: `app-description${key}`, + label: `app-label${key}`, lastModified: buildDate(id, 20), lastModifiedBy: `modifier${id}`, - version: key, name: `app-name${key}`, - canAccessApi: id % 2 === 0, - canAccessContent: id % 2 === 0, - description: `app-description${key}`, - label: `app-label${key}`, permissions: ['Owner'], roleName: `Role${id}`, roleProperties: createProperties(id), teamId: `app-team${key}`, + version: id, _links: { update: { method: 'PUT', href: `apps/${id}` }, }, @@ -389,18 +385,19 @@ describe('AppsService', () => { const key = `${id}${suffix}`; return { - hideScheduler: false, - patterns: [1, 2, 3].map(i => { - const name = `pattern${i}${key}`; - - return { name, regex: `${name}_regex`, message: `${name}_message` }; - }), editors: [1, 2, 3].map(i => { const name = `editor${i}${key}`; return { name, url: `${name}_url` }; }), - version: key, + hideDateTimeModeButton: true, + hideScheduler: true, + patterns: [1, 2, 3].map(i => { + const name = `pattern${i}${key}`; + + return { name, regex: `${name}_regex`, message: `${name}_message` }; + }), + version: id, _links: { update: { method: 'PUT', href: `apps/${id}/settings` }, }, @@ -411,7 +408,9 @@ describe('AppsService', () => { const key = `${id}${suffix}`; return { - update: key, + query: key, + queryPre: key, + version: id, _links: { update: { method: 'PUT', href: `apps/${id}/assets/scripts` }, }, @@ -420,58 +419,64 @@ describe('AppsService', () => { }); export function createApp(id: number, suffix = '') { - const links: ResourceLinks = { - update: { method: 'PUT', href: `apps/${id}` }, - }; - const key = `${id}${suffix}`; - return new AppDto(links, - `id${id}`, - DateTime.parseISO(buildDate(id, 10)), `creator${id}`, - DateTime.parseISO(buildDate(id, 20)), `modifier${id}`, - new Version(key), - `app-name${key}`, - `app-label${key}`, - `app-description${key}`, - ['Owner'], - id % 2 === 0, - id % 2 === 0, - `Role${id}`, - createProperties(id), - `app-team${key}`); + return new AppDto({ + id: `id${id}`, + canAccessApi: id % 2 === 0, + canAccessContent: id % 2 === 0, + created: DateTime.parseISO(buildDate(id, 10)), + createdBy: `creator${id}`, + description: `app-description${key}`, + label: `app-label${key}`, + lastModified: DateTime.parseISO(buildDate(id, 20)), + lastModifiedBy: `modifier${id}`, + name: `app-name${key}`, + permissions: ['Owner'], + roleName: `Role${id}`, + roleProperties: createProperties(id), + teamId: `app-team${key}`, + version: id, + _links: { + update: new ResourceLinkDto({ method: 'PUT', href: `apps/${id}` }), + }, + }); } export function createAppSettings(id: number, suffix = '') { - const links: ResourceLinks = { - update: { method: 'PUT', href: `apps/${id}/settings` }, - }; - const key = `${id}${suffix}`; - return new AppSettingsDto(links, - false, - [1, 2, 3].map(i => { - const name = `pattern${i}${key}`; + return new AppSettingsDto({ + editors: [1, 2, 3].map(i => { + const name = `editor${i}${key}`; - return new PatternDto(name, `${name}_regex`, `${name}_message`); + return new EditorDto({ name, url: `${name}_url` }); }), - [1, 2, 3].map(i => { - const name = `editor${i}${key}`; + hideDateTimeModeButton: true, + hideScheduler: true, + patterns: [1, 2, 3].map(i => { + const name = `pattern${i}${key}`; - return new EditorDto(name, `${name}_url`); + return new PatternDto({ name, regex: `${name}_regex`, message: `${name}_message` }); }), - new Version(key)); + version: id, + _links: { + update: new ResourceLinkDto({ method: 'PUT', href: `apps/${id}/settings` }), + }, + }); } -export function createAssetScripts(id: number, suffix = ''): AssetScriptsPayload { - return { - scripts: { update: `${id}${suffix}` }, +export function createAssetScripts(id: number, suffix = ''): AssetScriptsDto { + const key = `${id}${suffix}`; + + return new AssetScriptsDto({ + query: key, + queryPre: key, + version: id, _links: { - update: { method: 'PUT', href: `apps/${id}/assets/scripts` }, + update: new ResourceLinkDto({ method: 'PUT', href: `apps/${id}/assets/scripts` }), }, - canUpdate: true, - }; + }); } function createProperties(id: number) { diff --git a/frontend/src/app/shared/services/apps.service.ts b/frontend/src/app/shared/services/apps.service.ts index 878d5741a..f74110eec 100644 --- a/frontend/src/app/shared/services/apps.service.ts +++ b/frontend/src/app/shared/services/apps.service.ts @@ -9,153 +9,8 @@ import { HttpClient, HttpErrorResponse, HttpEventType, HttpResponse } from '@ang import { Injectable } from '@angular/core'; import { Observable, throwError } from 'rxjs'; import { catchError, filter, map } from 'rxjs/operators'; -import { ApiUrlConfig, DateTime, ErrorDto, getLinkUrl, hasAnyLink, HTTP, mapVersioned, pretifyError, Resource, ResourceLinks, StringHelper, Types, Version, Versioned } from '@app/framework'; - -export class AppDto { - public readonly _links: ResourceLinks; - - public readonly canCreateSchema: boolean; - public readonly canDelete: boolean; - public readonly canLeave: boolean; - public readonly canReadAssets: boolean; - public readonly canReadAssetsScripts: boolean; - public readonly canReadClients: boolean; - public readonly canReadContributors: boolean; - public readonly canReadJobs: boolean; - public readonly canReadLanguages: boolean; - public readonly canReadPatterns: boolean; - public readonly canReadPlans: boolean; - public readonly canReadRoles: boolean; - public readonly canReadRules: boolean; - public readonly canReadSchemas: boolean; - public readonly canReadWorkflows: boolean; - public readonly canUpdateTeam: boolean; - public readonly canUpdateGeneral: boolean; - public readonly canUpdateImage: boolean; - public readonly canUploadAssets: boolean; - - public readonly image: string; - - public readonly displayName: string; - - constructor(links: ResourceLinks, - public readonly id: string, - public readonly created: DateTime, - public readonly createdBy: string, - public readonly lastModified: DateTime, - public readonly lastModifiedBy: string, - public readonly version: Version, - public readonly name: string, - public readonly label: string | undefined, - public readonly description: string | undefined, - public readonly permissions: ReadonlyArray, - public readonly canAccessApi: boolean, - public readonly canAccessContent: boolean, - public readonly roleName: string | undefined, - public readonly roleProperties: { [key: string]: any }, - public readonly teamId: string | null, - ) { - this._links = links; - - this.displayName = StringHelper.firstNonEmpty(this.label, this.name); - - this.canCreateSchema = hasAnyLink(links, 'schemas/create'); - this.canDelete = hasAnyLink(links, 'delete'); - this.canLeave = hasAnyLink(links, 'leave'); - this.canReadAssets = hasAnyLink(links, 'assets'); - this.canReadAssetsScripts = hasAnyLink(links, 'assets/scripts'); - this.canReadClients = hasAnyLink(links, 'clients'); - this.canReadContributors = hasAnyLink(links, 'contributors'); - this.canReadJobs = hasAnyLink(links, 'jobs'); - this.canReadLanguages = hasAnyLink(links, 'languages'); - this.canReadPatterns = hasAnyLink(links, 'patterns'); - this.canReadPlans = hasAnyLink(links, 'plans'); - this.canReadRoles = hasAnyLink(links, 'roles'); - this.canReadRules = hasAnyLink(links, 'rules'); - this.canReadSchemas = hasAnyLink(links, 'schemas'); - this.canReadWorkflows = hasAnyLink(links, 'workflows'); - this.canUpdateGeneral = hasAnyLink(links, 'update'); - this.canUpdateImage = hasAnyLink(links, 'image/upload'); - this.canUpdateTeam = hasAnyLink(links, 'transfer'); - this.canUploadAssets = hasAnyLink(links, 'assets/create'); - this.image = getLinkUrl(links, 'image') as string; - } -} - -export class AppSettingsDto { - public readonly _links: ResourceLinks; - - public readonly canUpdate: boolean; - - constructor(links: ResourceLinks, - public readonly hideScheduler: boolean, - public readonly patterns: ReadonlyArray, - public readonly editors: ReadonlyArray, - public readonly version: Version, - ) { - this._links = links; - - this.canUpdate = hasAnyLink(links, 'update'); - } -} - -export class PatternDto { - constructor( - public readonly name: string, - public readonly regex: string, - public readonly message?: string, - ) { - } -} - -export class EditorDto { - constructor( - public readonly name: string, - public readonly url: string, - ) { - } -} - -export type AssetScripts = Readonly<{ [name: string]: string | null }>; - -export type AssetScriptsDto = Versioned; - -export type AssetScriptsPayload = Readonly<{ - // The actual asset scripts. - scripts: AssetScripts; - - // True, if the user has permissions to update the scripts. - canUpdate?: boolean; -}> & Resource; - -export type UpdateAppSettingsDto = Readonly<{ - // The regex patterns for scehams. - patterns: ReadonlyArray; - - // The registered editors for schemas. - editors: ReadonlyArray; - - // True if the scheduler should be hidden. - hideScheduler?: boolean; -}>; - -export type CreateAppDto = Readonly<{ - // The new name of the app. Must be unique. - name: string; -}>; - -export type TransferToTeamDto = Readonly<{ - // The target team ID. - teamId: string | null; -}>; - -export type UpdateAppDto = Readonly<{ - // The label, which is like a display name. - label?: string; - - // The description of the app. - description?: string; -}>; +import { ApiUrlConfig, ErrorDto, HTTP, mapVersioned, pretifyError, Resource, Types, Versioned, VersionOrTag } from '@app/framework'; +import { AppDto, AppSettingsDto, AssetScriptsDto, CreateAppDto, TransferToTeamDto, UpdateAppDto, UpdateAppSettingsDto, UpdateAssetScriptsDto } from './../model'; @Injectable({ providedIn: 'root', @@ -172,7 +27,7 @@ export class AppsService { return this.http.get(url).pipe( map(body => { - const apps = body.map(parseApp); + const apps = body.map(AppDto.fromJSON); return apps; }), @@ -184,7 +39,7 @@ export class AppsService { return this.http.get(url).pipe( map(body => { - const apps = body.map(parseApp); + const apps = body.map(AppDto.fromJSON); return apps; }), @@ -196,7 +51,7 @@ export class AppsService { return this.http.get(url).pipe( map(body => { - const app = parseApp(body); + const app = AppDto.fromJSON(body); return app; }), @@ -206,33 +61,33 @@ export class AppsService { public postApp(dto: CreateAppDto): Observable { const url = this.apiUrl.buildUrl('api/apps'); - return this.http.post(url, dto).pipe( + return this.http.post(url, dto.toJSON()).pipe( map(body => { - return parseApp(body); + return AppDto.fromJSON(body); }), pretifyError('i18n:apps.createFailed')); } - public putApp(appName: string, resource: Resource, dto: UpdateAppDto, version: Version): Observable { + public putApp(appName: string, resource: Resource, dto: UpdateAppDto, version: VersionOrTag): Observable { const link = resource._links['update']; const url = this.apiUrl.buildUrl(link.href); - return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe( + return HTTP.requestVersioned(this.http, link.method, url, version, dto.toJSON()).pipe( map(({ payload }) => { - return parseApp(payload.body); + return AppDto.fromJSON(payload.body); }), pretifyError('i18n:apps.updateFailed')); } - public transferApp(appName: string, resource: Resource, dto: TransferToTeamDto, version: Version): Observable { + public transferApp(appName: string, resource: Resource, dto: TransferToTeamDto, version: VersionOrTag): Observable { const link = resource._links['transfer']; const url = this.apiUrl.buildUrl(link.href); - return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe( + return HTTP.requestVersioned(this.http, link.method, url, version, dto.toJSON()).pipe( map(({ payload }) => { - return parseApp(payload.body); + return AppDto.fromJSON(payload.body); }), pretifyError('i18n:apps.transferFailed')); } @@ -242,46 +97,46 @@ export class AppsService { return this.http.get(url).pipe( map(body => { - return parseAppSettings(body); + return AppSettingsDto.fromJSON(body); }), pretifyError('i18n:apps.loadSettingsFailed')); } - public putSettings(appName: string, resource: Resource, dto: UpdateAppSettingsDto, version: Version): Observable { + public putSettings(appName: string, resource: Resource, dto: UpdateAppSettingsDto, version: VersionOrTag): Observable { const link = resource._links['update']; const url = this.apiUrl.buildUrl(link.href); - return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe( + return HTTP.requestVersioned(this.http, link.method, url, version, dto.toJSON()).pipe( map(({ payload }) => { - return parseAppSettings(payload.body); + return AppSettingsDto.fromJSON(payload.body); }), pretifyError('i18n:apps.updateSettingsFailed')); } - public getAssetScripts(appName: string): Observable { + public getAssetScripts(appName: string): Observable> { const url = this.apiUrl.buildUrl(`/api/apps/${appName}/assets/scripts`); return HTTP.getVersioned(this.http, url).pipe( mapVersioned(({ body }) => { - return parseAssetScripts(body); + return AssetScriptsDto.fromJSON(body); }), pretifyError('i18n:apps.loadAssetScriptsFailed')); } - public putAssetScripts(appName: string, resource: Resource, dto: AssetScripts, version: Version): Observable { + public putAssetScripts(appName: string, resource: Resource, dto: UpdateAssetScriptsDto, version: VersionOrTag): Observable> { const link = resource._links['update']; const url = this.apiUrl.buildUrl(link.href); - return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe( + return HTTP.requestVersioned(this.http, link.method, url, version, dto.toJSON()).pipe( mapVersioned(({ body }) => { - return parseAssetScripts(body); + return AssetScriptsDto.fromJSON(body); }), pretifyError('i18n:apps.updateAssetScriptsFailed')); } - public postAppImage(appName: string, resource: Resource, file: HTTP.UploadFile, version: Version): Observable { + public postAppImage(appName: string, resource: Resource, file: HTTP.UploadFile, version: VersionOrTag): Observable { const link = resource._links['image/upload']; const url = this.apiUrl.buildUrl(link.href); @@ -296,7 +151,7 @@ export class AppsService { return percentDone; } else if (Types.is(event, HttpResponse)) { - return parseApp(event.body); + return AppDto.fromJSON(event.body); } else { throw new Error('Invalid'); } @@ -311,14 +166,14 @@ export class AppsService { pretifyError('i18n:apps.uploadImageFailed')); } - public deleteAppImage(appName: string, resource: Resource, version: Version): Observable { + public deleteAppImage(appName: string, resource: Resource, version: VersionOrTag): Observable { const link = resource._links['image/delete']; const url = this.apiUrl.buildUrl(link.href); return HTTP.requestVersioned(this.http, link.method, url, version).pipe( map(({ payload }) => { - return parseApp(payload.body); + return AppDto.fromJSON(payload.body); }), pretifyError('i18n:apps.removeImageFailed')); } @@ -341,40 +196,3 @@ export class AppsService { pretifyError('i18n:apps.archiveFailed')); } } - -function parseApp(response: any & Resource) { - return new AppDto(response._links, - response.id, - DateTime.parseISO(response.created), response.createdBy, - DateTime.parseISO(response.lastModified), response.lastModifiedBy, - new Version(response.version.toString()), - response.name, - response.label, - response.description, - response.permissions, - response.canAccessApi, - response.canAccessContent, - response.roleName, - response.roleProperties, - response.teamId); -} - -function parseAppSettings(response: any & Resource) { - return new AppSettingsDto(response._links, - response.hideScheduler, - response.patterns.map((x: any) => { - return new PatternDto(x.name, x.regex, x.message); - }), - response.editors.map((x: any) => { - return new EditorDto(x.name, x.url); - }), - new Version(response.version.toString())); -} - -function parseAssetScripts(response: any): AssetScriptsPayload { - const { _links, ...scripts } = response; - - const canUpdate = hasAnyLink(_links, 'update'); - - return { scripts, canUpdate, _links }; -} diff --git a/frontend/src/app/shared/services/assets.service.spec.ts b/frontend/src/app/shared/services/assets.service.spec.ts index 3b57dcb15..2d3577c00 100644 --- a/frontend/src/app/shared/services/assets.service.spec.ts +++ b/frontend/src/app/shared/services/assets.service.spec.ts @@ -8,10 +8,11 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { ApiUrlConfig, AssetDto, AssetFolderDto, AssetFoldersDto, AssetsDto, AssetsService, DateTime, ErrorDto, MathHelper, Resource, ResourceLinks, sanitize, ScriptCompletions, Version } from '@app/shared/internal'; +import { ApiUrlConfig, AssetDto, AssetFolderDto, AssetFoldersDto, AssetsDto, AssetsService, DateTime, ErrorDto, MathHelper, Resource, sanitize, ScriptCompletions, VersionTag } from '@app/shared/internal'; +import { AnnotateAssetDto, AssetMetaDto, CreateAssetFolderDto, MoveAssetDto, MoveAssetFolderDto, RenameAssetFolderDto, RenameTagDto, ResourceLinkDto } from './../model'; describe('AssetsService', () => { - const version = new Version('1'); + const version = new VersionTag('1'); beforeEach(() => { TestBed.configureTestingModule({ @@ -107,17 +108,15 @@ describe('AssetsService', () => { it('should make put request to rename asset tag', inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => { - const dto = { tagName: 'new-name' }; + const dto = new RenameTagDto({ tagName: 'new-name' }); let tags: any; - assetsService.putTag('my-app', 'old-name', dto).subscribe(result => { tags = result; }); const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets/tags/old-name'); - expect(req.request.body).toEqual(dto); expect(req.request.method).toEqual('PUT'); expect(req.request.headers.get('If-Match')).toBeNull(); @@ -136,7 +135,6 @@ describe('AssetsService', () => { it(`should make post request to get assets with ${x.name}`, inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => { let assets: AssetsDto; - assetsService.getAssets('my-app', x.query).subscribe(result => { assets = result; }); @@ -155,28 +153,23 @@ describe('AssetsService', () => { assetResponse(12), assetResponse(13), ], - folders: [ - assetFolderResponse(22), - assetFolderResponse(23), - ], + _links: {}, }); - expect(assets!).toEqual({ + expect(assets!).toEqual(new AssetsDto({ + total: 10, items: [ createAsset(12), createAsset(13), ], - total: 10, - canCreate: false, - canRenameTag: false, - }); + _links: {}, + })); })); }); it('should make get request to get asset folders', inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => { let assetFolders: AssetFoldersDto; - assetsService.getAssetFolders('my-app', 'parent1', 'Path').subscribe(result => { assetFolders = result; }); @@ -195,9 +188,11 @@ describe('AssetsService', () => { path: [ assetFolderResponse(44), ], + _links: {}, }); - expect(assetFolders!).toEqual({ + expect(assetFolders!).toEqual(new AssetFoldersDto({ + total: 10, items: [ createAssetFolder(22), createAssetFolder(23), @@ -205,14 +200,13 @@ describe('AssetsService', () => { path: [ createAssetFolder(44), ], - canCreate: false, - }); + _links: {}, + })); })); it('should make get request to get asset', inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => { let asset: AssetDto; - assetsService.getAsset('my-app', '123').subscribe(result => { asset = result; }); @@ -230,7 +224,6 @@ describe('AssetsService', () => { it('should make post request to create asset', inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => { let asset: AssetDto; - assetsService.postAssetFile('my-app', null!).subscribe(result => { asset = result; }); @@ -248,7 +241,6 @@ describe('AssetsService', () => { it('should make post with parent id to create asset', inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => { let asset: AssetDto; - assetsService.postAssetFile('my-app', null!, 'parent1').subscribe(result => { asset = result; }); @@ -266,7 +258,6 @@ describe('AssetsService', () => { it('should return proper error if upload failed with 413', inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => { let error: ErrorDto; - assetsService.postAssetFile('my-app', null!).subscribe({ error: e => { error = e; @@ -292,7 +283,6 @@ describe('AssetsService', () => { }; let asset: AssetDto; - assetsService.putAssetFile('my-app', resource, null!, version).subscribe(result => { asset = result; }); @@ -316,7 +306,6 @@ describe('AssetsService', () => { }; let error: ErrorDto; - assetsService.putAssetFile('my-app', resource, null!, version).subscribe({ error: e => { error = e; @@ -335,7 +324,7 @@ describe('AssetsService', () => { it('should make put request to annotate asset', inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => { - const dto = { fileName: 'New-Name.png' }; + const dto = new AnnotateAssetDto({ fileName: 'New-Name.png' }); const resource: Resource = { _links: { @@ -344,7 +333,6 @@ describe('AssetsService', () => { }; let asset: AssetDto; - assetsService.putAsset('my-app', resource, dto, version).subscribe(result => { asset = result; }); @@ -361,6 +349,8 @@ describe('AssetsService', () => { it('should make put request to move asset', inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => { + const dto = new MoveAssetDto({ parentId: 'parent1' }); + const resource: Resource = { _links: { move: { method: 'PUT', href: 'api/apps/my-app/assets/123/parent' }, @@ -368,8 +358,7 @@ describe('AssetsService', () => { }; let asset: AssetDto; - - assetsService.putAssetParent('my-app', resource, { parentId: 'parent1' }, version).subscribe(result => { + assetsService.putAssetParent('my-app', resource, dto, version).subscribe(result => { asset = result; }); @@ -385,6 +374,8 @@ describe('AssetsService', () => { it('should make put request to move asset folder', inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => { + const dto = new MoveAssetFolderDto({ parentId: 'parent1' }); + const resource: Resource = { _links: { move: { method: 'PUT', href: 'api/apps/my-app/assets/folders/123/parent' }, @@ -392,8 +383,7 @@ describe('AssetsService', () => { }; let assetFolder: AssetFolderDto; - - assetsService.putAssetFolderParent('my-app', resource, { parentId: 'parent1' }, version).subscribe(result => { + assetsService.putAssetFolderParent('my-app', resource, dto, version).subscribe(result => { assetFolder = result; }); @@ -427,10 +417,9 @@ describe('AssetsService', () => { it('should make post request to create asset folder', inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => { - const dto = { folderName: 'My Folder' }; + const dto = new CreateAssetFolderDto({ folderName: 'My Folder' }); let assetFolder: AssetFolderDto; - assetsService.postAssetFolder('my-app', dto).subscribe(result => { assetFolder = result; }); @@ -447,7 +436,7 @@ describe('AssetsService', () => { it('should make put request to update asset folder', inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => { - const dto = { folderName: 'My Folder' }; + const dto = new RenameAssetFolderDto({ folderName: 'My Folder' }); const resource: Resource = { _links: { @@ -456,7 +445,6 @@ describe('AssetsService', () => { }; let assetFolder: AssetFolderDto; - assetsService.putAssetFolder('my-app', resource, dto, version).subscribe(result => { assetFolder = result; }); @@ -474,7 +462,6 @@ describe('AssetsService', () => { it('should make get request to get completions', inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => { let completions: ScriptCompletions; - assetsService.getCompletions('my-app').subscribe(result => { completions = result; }); @@ -498,27 +485,28 @@ describe('AssetsService', () => { id: `id${id}`, created: buildDate(id, 10), createdBy: `creator${id}`, - lastModified: buildDate(id, 20), - lastModifiedBy: `modifier${id}`, - fileName: `My Name${key}.png`, fileHash: `My Hash${key}`, - fileType: 'png', + fileName: `My Name${key}.png`, fileSize: id * 2, + fileType: 'png', fileVersion: id * 4, + isImage: false, isProtected: true, - parentId, + lastModified: buildDate(id, 20), + lastModifiedBy: `modifier${id}`, mimeType: 'image/png', - type: `my-type${key}`, metadataText: `my-metadata${key}`, metadata: { pixelWidth: id * 3, pixelHeight: id * 5, }, + parentId, slug: `my-name${key}.png`, tags: [ 'tag1', 'tag2', ], + type: `my-type${key}`, version: id, _links: { update: { method: 'PUT', href: `/assets/${id}` }, @@ -546,43 +534,45 @@ describe('AssetsService', () => { } }); -export function createAsset(id: number, tags?: ReadonlyArray, suffix = '', parentId?: string) { - const links: ResourceLinks = { - update: { method: 'PUT', href: `/assets/${id}` }, - }; +export function createAsset(id: number, tags?: string[], suffix = '', parentId?: string) { + parentId = parentId || MathHelper.EMPTY_GUID; const key = `${id}${suffix}`; - const meta = { - isDuplicate: 'true', - }; - - parentId = parentId || MathHelper.EMPTY_GUID; - - return new AssetDto(links, meta, - `id${id}`, - DateTime.parseISO(buildDate(id, 10)), `creator${id}`, - DateTime.parseISO(buildDate(id, 20)), `modifier${id}`, - new Version(key), - `My Name${key}.png`, - `My Hash${key}`, - 'png', - id * 2, - id * 4, - true, - parentId, - 'image/png', - `my-type${key}`, - `my-metadata${key}`, - { + return new AssetDto({ + id: `id${id}`, + created: DateTime.parseISO(buildDate(id, 10)), + createdBy: `creator${id}`, + fileHash: `My Hash${key}`, + fileName: `My Name${key}.png`, + fileSize: id * 2, + fileType: 'png', + fileVersion: id * 4, + isImage: false, + isProtected: true, + lastModified: DateTime.parseISO(buildDate(id, 20)), + lastModifiedBy: `modifier${id}`, + mimeType: 'image/png', + metadataText: `my-metadata${key}`, + metadata: { pixelWidth: id * 3, pixelHeight: id * 5, }, - `my-name${key}.png`, - tags || [ + parentId, + slug: `my-name${key}.png`, + tags: tags || [ 'tag1', 'tag2', - ]); + ], + type: `my-type${key}` as any, + version: id, + _links: { + update: new ResourceLinkDto({ method: 'PUT', href: `/assets/${id}` }), + }, + _meta: new AssetMetaDto({ + isDuplicate: 'true', + }), + }); } export function createAssetFolder(id: number, suffix = '', parentId?: string) { @@ -590,11 +580,15 @@ export function createAssetFolder(id: number, suffix = '', parentId?: string) { const key = `${id}${suffix}`; - const links: ResourceLinks = { - update: { method: 'PUT', href: `/assets/folders/${id}` }, - }; - - return new AssetFolderDto(links, `id${id}`, `My Folder${key}`, parentId, new Version(`${id}`)); + return new AssetFolderDto({ + id: `id${id}`, + folderName: `My Folder${key}`, + parentId, + version: id, + _links: { + update: new ResourceLinkDto({ method: 'PUT', href: `/assets/folders/${id}` }), + }, + }); } function buildDate(id: number, add = 0) { diff --git a/frontend/src/app/shared/services/assets.service.ts b/frontend/src/app/shared/services/assets.service.ts index 8ecf6eedf..5f3d2a21e 100644 --- a/frontend/src/app/shared/services/assets.service.ts +++ b/frontend/src/app/shared/services/assets.service.ts @@ -9,168 +9,12 @@ import { HttpClient, HttpErrorResponse, HttpEventType, HttpResponse } from '@ang import { Injectable } from '@angular/core'; import { Observable, throwError } from 'rxjs'; import { catchError, filter, map } from 'rxjs/operators'; -import { ApiUrlConfig, DateTime, ErrorDto, getLinkUrl, hasAnyLink, HTTP, Metadata, pretifyError, Resource, ResourceLinks, ScriptCompletions, StringHelper, Types, Version, Versioned } from '@app/framework'; +import { ApiUrlConfig, ErrorDto, HTTP, pretifyError, Resource, ScriptCompletions, StringHelper, Types, Versioned, VersionOrTag } from '@app/framework'; +import { AnnotateAssetDto, AssetDto, AssetFolderDto, AssetFoldersDto, AssetsDto, CreateAssetFolderDto, MoveAssetDto, MoveAssetFolderDto, RenameAssetFolderDto, RenameTagDto } from './../model'; import { Query, sanitize } from './query'; -const SVG_PREVIEW_LIMIT = 10 * 1024; - -const MIME_TIFF = 'image/tiff'; -const MIME_SVG = 'image/svg+xml'; - type AssetFolderScope = 'PathAndItems' | 'Path' | 'Items'; -export class AssetDto { - public readonly _meta: Metadata = {}; - - public readonly _links: ResourceLinks; - - public readonly canDelete: boolean; - public readonly canPreview: boolean; - public readonly canUpdate: boolean; - public readonly canUpload: boolean; - public readonly canMove: boolean; - - public get isDuplicate() { - return this._meta && this._meta['isDuplicate'] === 'true'; - } - - public get contentUrl() { - return getLinkUrl(this, 'content') as string; - } - - public get fileNameWithoutExtension() { - const index = this.fileName.lastIndexOf('.'); - - if (index > 0) { - return this.fileName.substring(0, index); - } else { - return this.fileName; - } - } - - constructor(links: ResourceLinks, meta: Metadata, - public readonly id: string, - public readonly created: DateTime, - public readonly createdBy: string, - public readonly lastModified: DateTime, - public readonly lastModifiedBy: string, - public readonly version: Version, - public readonly fileName: string, - public readonly fileHash: string, - public readonly fileType: string, - public readonly fileSize: number, - public readonly fileVersion: number, - public readonly isProtected: boolean, - public readonly parentId: string, - public readonly mimeType: string, - public readonly type: string, - public readonly metadataText: string, - public readonly metadata: any, - public readonly slug: string, - public readonly tags: ReadonlyArray, - ) { - this.canPreview = - (this.mimeType !== MIME_TIFF && this.type === 'Image') || - (this.mimeType === MIME_SVG && this.fileSize < SVG_PREVIEW_LIMIT); - - this._links = links; - - this.canDelete = hasAnyLink(links, 'delete'); - this.canUpdate = hasAnyLink(links, 'update'); - this.canUpload = hasAnyLink(links, 'upload'); - this.canMove = hasAnyLink(links, 'move'); - - this._meta = meta; - } - - public fullUrl(apiUrl: ApiUrlConfig) { - return apiUrl.buildUrl(this.contentUrl); - } -} - -export class AssetFolderDto { - public readonly _links: ResourceLinks; - - public readonly canDelete: boolean; - public readonly canUpdate: boolean; - public readonly canMove: boolean; - - constructor(links: ResourceLinks, - public readonly id: string, - public readonly folderName: string, - public readonly parentId: string, - public readonly version: Version, - ) { - this._links = links; - - this.canDelete = hasAnyLink(links, 'delete'); - this.canUpdate = hasAnyLink(links, 'update'); - this.canMove = hasAnyLink(links, 'move'); - } -} - -export type AssetsDto = Readonly<{ - // The list of assets. - items: ReadonlyArray; - - // The total number of assets. - total: number; - - // True, if the user has permissions to create an asset. - canCreate?: boolean; - - // True, if the user has permissions to rename a tag. - canRenameTag?: boolean; -}>; - -export type AssetFoldersDto = Readonly<{ - // The list of asset folders. - items: ReadonlyArray; - - // The path to the asset folders. - path: ReadonlyArray; - - // True, if the user has permissions to create an asset folder. - canCreate?: boolean; -}>; - -export type AnnotateAssetDto = Readonly<{ - // The optional file name. - fileName?: string; - - // The optional flag, if an asset is protected. - isProtected?: boolean; - - // The optiona slug. - slug?: string; - - // The optional tags. - tags?: ReadonlyArray; - - // The optional metadata. - metadata?: { [key: string]: any }; -}>; - -export type CreateAssetFolderDto = Readonly<{ - // The name of the folder. - folderName: string; -} & MoveAssetItemDto>; - -export type RenameAssetFolderDto = Readonly<{ - // The name of the folder. - folderName: string; -}>; - -export type RenameAssetTagDto = Readonly<{ - // The name of the tag. - tagName: string; -}>; - -export type MoveAssetItemDto = Readonly<{ - // The new ID to the asset folder. - parentId?: string; -}>; - export type AssetsQuery = Readonly<{ // True, to not return the total number of items. noTotal?: boolean; @@ -216,10 +60,10 @@ export class AssetsService { ) { } - public putTag(appName: string, name: string, dto: RenameAssetTagDto): Observable<{ [name: string]: number }> { + public putTag(appName: string, name: string, dto: RenameTagDto): Observable<{ [name: string]: number }> { const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets/tags/${encodeURIComponent(name)}`); - return this.http.put<{ [name: string]: number }>(url, dto).pipe( + return this.http.put<{ [name: string]: number }>(url, dto.toJSON()).pipe( pretifyError('i18n:assets.renameTagFailed')); } @@ -237,7 +81,7 @@ export class AssetsService { return this.http.post(url, body, buildHeaders(q, (q as any)?.ref)).pipe( map(body => { - return parseAssets(body); + return AssetsDto.fromJSON(body); }), pretifyError('i18n:assets.loadFailed')); } @@ -247,7 +91,7 @@ export class AssetsService { return this.http.get(url).pipe( map(body => { - return parseAssetFolders(body); + return AssetFoldersDto.fromJSON(body); }), pretifyError('i18n:assets.loadFoldersFailed')); } @@ -257,7 +101,7 @@ export class AssetsService { return HTTP.getVersioned(this.http, url).pipe( map(({ payload }) => { - return parseAsset(payload.body); + return AssetDto.fromJSON(payload.body); }), pretifyError('i18n:assets.loadFailed')); } @@ -275,7 +119,7 @@ export class AssetsService { return percentDone; } else if (Types.is(event, HttpResponse)) { - return parseAsset(event.body); + return AssetDto.fromJSON(event.body); } else { throw new Error('Invalid'); } @@ -290,7 +134,7 @@ export class AssetsService { pretifyError('i18n:assets.uploadFailed')); } - public putAssetFile(appName: string, resource: Resource, file: HTTP.UploadFile, version: Version): Observable { + public putAssetFile(appName: string, resource: Resource, file: HTTP.UploadFile, version: VersionOrTag): Observable { const link = resource._links['upload']; const url = this.apiUrl.buildUrl(link.href); @@ -305,7 +149,7 @@ export class AssetsService { return percentDone; } else if (Types.is(event, HttpResponse)) { - return parseAsset(event.body); + return AssetDto.fromJSON(event.body); } else { throw new Error('Invalid'); } @@ -323,62 +167,62 @@ export class AssetsService { public postAssetFolder(appName: string, dto: CreateAssetFolderDto): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets/folders`); - return HTTP.postVersioned(this.http, url, dto).pipe( + return HTTP.postVersioned(this.http, url, dto.toJSON()).pipe( map(({ payload }) => { - return parseAssetFolder(payload.body); + return AssetFolderDto.fromJSON(payload.body); }), pretifyError('i18n:assets.createFolderFailed')); } - public putAsset(appName: string, resource: Resource, dto: AnnotateAssetDto, version: Version): Observable { + public putAsset(appName: string, resource: Resource, dto: AnnotateAssetDto, version: VersionOrTag): Observable { const link = resource._links['update']; const url = this.apiUrl.buildUrl(link.href); - return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe( + return HTTP.requestVersioned(this.http, link.method, url, version, dto.toJSON()).pipe( map(({ payload }) => { - return parseAsset(payload.body); + return AssetDto.fromJSON(payload.body); }), pretifyError('i18n:assets.updateFailed')); } - public putAssetFolder(appName: string, resource: Resource, dto: RenameAssetFolderDto, version: Version): Observable { + public putAssetFolder(appName: string, resource: Resource, dto: RenameAssetFolderDto, version: VersionOrTag): Observable { const link = resource._links['update']; const url = this.apiUrl.buildUrl(link.href); - return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe( + return HTTP.requestVersioned(this.http, link.method, url, version, dto.toJSON()).pipe( map(({ payload }) => { - return parseAssetFolder(payload.body); + return AssetFolderDto.fromJSON(payload.body); }), pretifyError('i18n:assets.updateFolderFailed')); } - public putAssetParent(appName: string, resource: Resource, dto: MoveAssetItemDto, version: Version): Observable { + public putAssetParent(appName: string, resource: Resource, dto: MoveAssetDto, version: VersionOrTag): Observable { const link = resource._links['move']; const url = this.apiUrl.buildUrl(link.href); - return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe( + return HTTP.requestVersioned(this.http, link.method, url, version, dto.toJSON()).pipe( map(({ payload }) => { - return parseAsset(payload.body); + return AssetDto.fromJSON(payload.body); }), pretifyError('i18n:assets.moveFailed')); } - public putAssetFolderParent(appName: string, resource: Resource, dto: MoveAssetItemDto, version: Version): Observable { + public putAssetFolderParent(appName: string, resource: Resource, dto: MoveAssetFolderDto, version: VersionOrTag): Observable { const link = resource._links['move']; const url = this.apiUrl.buildUrl(link.href); - return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe( + return HTTP.requestVersioned(this.http, link.method, url, version, dto.toJSON()).pipe( map(({ payload }) => { - return parseAssetFolder(payload.body); + return AssetFolderDto.fromJSON(payload.body); }), pretifyError('i18n:assets.moveFailed')); } - public deleteAssetItem(appName: string, asset: Resource, checkReferrers: boolean, version: Version): Observable> { + public deleteAssetItem(appName: string, asset: Resource, checkReferrers: boolean, version: VersionOrTag): Observable> { const link = asset._links['delete']; const url = `${this.apiUrl.buildUrl(link.href)}${StringHelper.buildQuery({ checkReferrers })}`; @@ -462,52 +306,4 @@ function buildQuery(q?: AssetsQuery & AssetsByQuery & AssetsByIds & AssetsByRef) return { q: sanitize(queryObj), parentId }; } -} - -function parseAssets(response: { items: any[]; total: number } & Resource): AssetsDto { - const { items: list, total, _links } = response; - const items = list.map(parseAsset); - - const canCreate = hasAnyLink(_links, 'create'); - const canRenameTag = hasAnyLink(_links, 'tags/rename'); - - return { items, total, canCreate, canRenameTag }; -} - -function parseAsset(response: any) { - return new AssetDto(response._links, response._meta, - response.id, - DateTime.parseISO(response.created), response.createdBy, - DateTime.parseISO(response.lastModified), response.lastModifiedBy, - new Version(response.version.toString()), - response.fileName, - response.fileHash, - response.fileType, - response.fileSize, - response.fileVersion, - response.isProtected, - response.parentId, - response.mimeType, - response.type, - response.metadataText, - response.metadata, - response.slug, - response.tags || []); -} - -function parseAssetFolders(response: { items: any[]; path: any[]; total: number } & Resource): AssetFoldersDto { - const { items: list, _links } = response; - const items = list.map(parseAssetFolder); - - const canCreate = hasAnyLink(_links, 'create'); - - return { items, canCreate, path: response.path.map(parseAssetFolder) }; -} - -function parseAssetFolder(response: any) { - return new AssetFolderDto(response._links, - response.id, - response.folderName, - response.parentId, - new Version(response.version.toString())); -} +} \ No newline at end of file diff --git a/frontend/src/app/shared/services/autosave.service.spec.ts b/frontend/src/app/shared/services/autosave.service.spec.ts index d66cedc1c..12d009741 100644 --- a/frontend/src/app/shared/services/autosave.service.spec.ts +++ b/frontend/src/app/shared/services/autosave.service.spec.ts @@ -6,7 +6,7 @@ */ import { IMock, It, Mock, Times } from 'typemoq'; -import { AutoSaveService, LocalStoreService, Version } from '@app/shared/internal'; +import { AutoSaveService, LocalStoreService } from '@app/shared/internal'; describe('AutoSaveService', () => { let localStore: IMock; @@ -20,7 +20,7 @@ describe('AutoSaveService', () => { }); it('should remove unsaved created content', () => { - autoSaveService.remove({ schemaId: '1', schemaVersion: new Version('2') }); + autoSaveService.remove({ schemaId: '1', schemaVersion: 2 }); expect().nothing(); @@ -28,7 +28,7 @@ describe('AutoSaveService', () => { }); it('should remove unsaved edited content', () => { - autoSaveService.remove({ schemaId: '1', schemaVersion: new Version('2'), contentId: '3' }); + autoSaveService.remove({ schemaId: '1', schemaVersion: 2, contentId: '3' }); expect().nothing(); @@ -44,7 +44,7 @@ describe('AutoSaveService', () => { }); it('should save unsaved created content', () => { - autoSaveService.set({ schemaId: '1', schemaVersion: new Version('2') }, { text: 'Hello' }); + autoSaveService.set({ schemaId: '1', schemaVersion: 2 }, { text: 'Hello' }); expect().nothing(); @@ -52,7 +52,7 @@ describe('AutoSaveService', () => { }); it('should save unsaved edited content', () => { - autoSaveService.set({ schemaId: '1', schemaVersion: new Version('2'), contentId: '3' }, { text: 'Hello' }); + autoSaveService.set({ schemaId: '1', schemaVersion: 2, contentId: '3' }, { text: 'Hello' }); expect().nothing(); @@ -68,7 +68,7 @@ describe('AutoSaveService', () => { }); it('should not save content if content is not defined', () => { - autoSaveService.set({ schemaId: '1', schemaVersion: new Version('2') }, null!); + autoSaveService.set({ schemaId: '1', schemaVersion: 2 }, null!); expect().nothing(); @@ -79,7 +79,7 @@ describe('AutoSaveService', () => { localStore.setup(x => x.get('autosave.1-2')) .returns(() => '{"text":"Hello"}'); - const key = { schemaId: '1', schemaVersion: new Version('2') }; + const key = { schemaId: '1', schemaVersion: 2 }; const content = autoSaveService.fetch(key); @@ -92,7 +92,7 @@ describe('AutoSaveService', () => { localStore.setup(x => x.get('autosave.1-2.3')) .returns(() => '{"text":"Hello"}'); - const key = { schemaId: '1', schemaVersion: new Version('2'), contentId: '3' }; + const key = { schemaId: '1', schemaVersion: 2, contentId: '3' }; const content = autoSaveService.fetch(key); diff --git a/frontend/src/app/shared/services/autosave.service.ts b/frontend/src/app/shared/services/autosave.service.ts index 6fb14281d..39d0ff924 100644 --- a/frontend/src/app/shared/services/autosave.service.ts +++ b/frontend/src/app/shared/services/autosave.service.ts @@ -65,5 +65,5 @@ function getKey(key: AutoSaveKey) { contentId = `.${contentId}`; } - return `autosave.${schemaId}-${schemaVersion.value}${contentId}`; + return `autosave.${schemaId}-${schemaVersion}${contentId}`; } diff --git a/frontend/src/app/shared/services/clients.service.spec.ts b/frontend/src/app/shared/services/clients.service.spec.ts index 49d5ce00e..1002459db 100644 --- a/frontend/src/app/shared/services/clients.service.spec.ts +++ b/frontend/src/app/shared/services/clients.service.spec.ts @@ -8,10 +8,11 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { AccessTokenDto, ApiUrlConfig, ClientDto, ClientsDto, ClientsPayload, ClientsService, Resource, ResourceLinks, Version } from '@app/shared/internal'; +import { AccessTokenDto, ApiUrlConfig, ClientDto, ClientsDto, ClientsService, Resource, Versioned, VersionTag } from '@app/shared/internal'; +import { CreateClientDto, ResourceLinkDto, UpdateClientDto } from '../model'; describe('ClientsService', () => { - const version = new Version('1'); + const version = new VersionTag('1'); beforeEach(() => { TestBed.configureTestingModule({ @@ -31,8 +32,7 @@ describe('ClientsService', () => { it('should make get request to get app clients', inject([ClientsService, HttpTestingController], (clientsService: ClientsService, httpMock: HttpTestingController) => { - let clients: ClientsDto; - + let clients: Versioned; clientsService.getClients('my-app').subscribe(result => { clients = result; }); @@ -48,15 +48,14 @@ describe('ClientsService', () => { }, }); - expect(clients!).toEqual({ payload: createClients(1, 2), version: new Version('2') }); + expect(clients!).toEqual({ payload: createClients(1, 2), version: new VersionTag('2') }); })); it('should make post request to create client', inject([ClientsService, HttpTestingController], (clientsService: ClientsService, httpMock: HttpTestingController) => { - const dto = { id: 'client1' }; - - let clients: ClientsDto; + const dto = new CreateClientDto({ id: 'client1' }); + let clients: Versioned; clientsService.postClient('my-app', dto, version).subscribe(result => { clients = result; }); @@ -72,12 +71,12 @@ describe('ClientsService', () => { }, }); - expect(clients!).toEqual({ payload: createClients(1, 2), version: new Version('2') }); + expect(clients!).toEqual({ payload: createClients(1, 2), version: new VersionTag('2') }); })); it('should make put request to rename client', inject([ClientsService, HttpTestingController], (clientsService: ClientsService, httpMock: HttpTestingController) => { - const dto = { name: 'New Name' }; + const dto = new UpdateClientDto({ name: 'New Name' }); const resource: Resource = { _links: { @@ -85,8 +84,7 @@ describe('ClientsService', () => { }, }; - let clients: ClientsDto; - + let clients: Versioned; clientsService.putClient('my-app', resource, dto, version).subscribe(result => { clients = result; }); @@ -102,7 +100,7 @@ describe('ClientsService', () => { }, }); - expect(clients!).toEqual({ payload: createClients(1, 2), version: new Version('2') }); + expect(clients!).toEqual({ payload: createClients(1, 2), version: new VersionTag('2') }); })); it('should make delete request to remove client', @@ -113,8 +111,7 @@ describe('ClientsService', () => { }, }; - let clients: ClientsDto; - + let clients: Versioned; clientsService.deleteClient('my-app', resource, version).subscribe(result => { clients = result; }); @@ -130,7 +127,7 @@ describe('ClientsService', () => { }, }); - expect(clients!).toEqual({ payload: createClients(1, 2), version: new Version('2') }); + expect(clients!).toEqual({ payload: createClients(1, 2), version: new VersionTag('2') }); })); it('should make form request to create token', @@ -174,17 +171,26 @@ describe('ClientsService', () => { } }); -export function createClients(...ids: ReadonlyArray): ClientsPayload { - return { +export function createClients(...ids: ReadonlyArray) { + return new ClientsDto({ items: ids.map(createClient), - canCreate: true, - }; + _links: { + create: new ResourceLinkDto({ method: 'POST', href: '/clients' }), + }, + }); } export function createClient(id: number) { - const links: ResourceLinks = { - update: { method: 'PUT', href: `/clients/id${id}` }, - }; - - return new ClientDto(links, `id${id}`, `Client ${id}`, `secret${id}`, `Role${id}`, id * 512, id * 5120, true); -} + return new ClientDto({ + id: `id${id}`, + name: `Client ${id}`, + role: `Role${id}`, + secret: `secret${id}`, + apiCallsLimit: id * 512, + apiTrafficLimit: id * 5120, + allowAnonymous: true, + _links: { + update: new ResourceLinkDto({ method: 'PUT', href: `/clients/id${id}` }), + }, + }); +} \ No newline at end of file diff --git a/frontend/src/app/shared/services/clients.service.ts b/frontend/src/app/shared/services/clients.service.ts index c04132f32..ebb3bd34f 100644 --- a/frontend/src/app/shared/services/clients.service.ts +++ b/frontend/src/app/shared/services/clients.service.ts @@ -9,29 +9,8 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { ApiUrlConfig, hasAnyLink, HTTP, mapVersioned, pretifyError, Resource, ResourceLinks, Version, Versioned } from '@app/framework'; - -export class ClientDto { - public readonly _links: ResourceLinks; - - public readonly canRevoke: boolean; - public readonly canUpdate: boolean; - - constructor(links: ResourceLinks, - public readonly id: string, - public readonly name: string, - public readonly secret: string, - public readonly role: string, - public readonly apiCallsLimit: number, - public readonly apiTrafficLimit: number, - public readonly allowAnonymous: boolean, - ) { - this._links = links; - - this.canRevoke = hasAnyLink(links, 'delete'); - this.canUpdate = hasAnyLink(links, 'update'); - } -} +import { ApiUrlConfig, HTTP, mapVersioned, pretifyError, Resource, Versioned, VersionOrTag } from '@app/framework'; +import { ClientDto, ClientsDto, CreateClientDto, UpdateClientDto } from './../model'; export class AccessTokenDto { constructor( @@ -41,35 +20,6 @@ export class AccessTokenDto { } } -export type ClientsDto = Versioned; - -export type ClientsPayload = Readonly<{ - // The list of clients. - items: ReadonlyArray; - - // True if the user has permissions to create a client. - canCreate?: boolean; -}>; - -export type CreateClientDto = Readonly<{ - // The new client ID. - id: string; - }>; - -export type UpdateClientDto = Readonly<{ - // The optional client name. - name?: string; - - // The role for the client to define the permissions. - role?: string; - - // True if the client can be used for anonymous access. - allowAnonymous?: boolean; - - // The allowed api calls. - apiCallsLimit?: number; -}>; - @Injectable({ providedIn: 'root', }) @@ -80,46 +30,46 @@ export class ClientsService { ) { } - public getClients(appName: string): Observable { + public getClients(appName: string): Observable> { const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients`); return HTTP.getVersioned(this.http, url).pipe( mapVersioned(({ body }) => { - return parseClients(body); + return ClientsDto.fromJSON(body); }), pretifyError('i18n:clients.loadFailed')); } - public postClient(appName: string, dto: CreateClientDto, version: Version): Observable { + public postClient(appName: string, dto: CreateClientDto, version: VersionOrTag): Observable> { const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients`); - return HTTP.postVersioned(this.http, url, dto, version).pipe( + return HTTP.postVersioned(this.http, url, dto.toJSON(), version).pipe( mapVersioned(({ body }) => { - return parseClients(body); + return ClientsDto.fromJSON(body); }), pretifyError('i18n:clients.addFailed')); } - public putClient(appName: string, resource: Resource, dto: UpdateClientDto, version: Version): Observable { + public putClient(appName: string, resource: Resource, dto: UpdateClientDto, version: VersionOrTag): Observable> { const link = resource._links['update']; const url = this.apiUrl.buildUrl(link.href); - return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe( + return HTTP.requestVersioned(this.http, link.method, url, version, dto.toJSON()).pipe( mapVersioned(({ body }) => { - return parseClients(body); + return ClientsDto.fromJSON(body); }), pretifyError('i18n:clients.revokeFailed')); } - public deleteClient(appName: string, resource: Resource, version: Version): Observable { + public deleteClient(appName: string, resource: Resource, version: VersionOrTag): Observable> { const link = resource._links['delete']; const url = this.apiUrl.buildUrl(link.href); return HTTP.requestVersioned(this.http, link.method, url, version).pipe( mapVersioned(({ body }) => { - return parseClients(body); + return ClientsDto.fromJSON(body); }), pretifyError('i18n:clients.revokeFailed')); } @@ -141,24 +91,4 @@ export class ClientsService { }), pretifyError('i18n:clients.tokenFailed')); } -} - -function parseClients(response: { items: any[] } & Resource): ClientsPayload { - const { items: list, _links } = response; - const items = list.map(parseClient); - - const canCreate = hasAnyLink(_links, 'create'); - - return { items, canCreate }; -} - -function parseClient(response: any) { - return new ClientDto(response._links, - response.id, - response.name || response.id, - response.secret, - response.role, - response.apiCallsLimit, - response.apiTrafficLimit, - response.allowAnonymous); -} +} \ No newline at end of file diff --git a/frontend/src/app/shared/services/contents.service.spec.ts b/frontend/src/app/shared/services/contents.service.spec.ts index 251f4d360..fbcfbac59 100644 --- a/frontend/src/app/shared/services/contents.service.spec.ts +++ b/frontend/src/app/shared/services/contents.service.spec.ts @@ -8,13 +8,12 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { ErrorDto } from '@app/framework'; -import { ApiUrlConfig, ContentDto, ContentsDto, ContentsService, DateTime, Resource, ResourceLinks, ScheduleDto, Version, Versioned } from '@app/shared/internal'; -import { BulkResultDto, BulkUpdateDto } from './contents.service'; +import { ApiUrlConfig, ContentDto, ContentsDto, ContentsService, DateTime, Resource, ScheduleJobDto, Versioned, VersionTag } from '@app/shared/internal'; +import { BulkResultDto, BulkUpdateContentsDto, BulkUpdateContentsJobDto, ResourceLinkDto, ServerErrorDto, StatusInfoDto } from './../model'; import { sanitize } from './query'; describe('ContentsService', () => { - const version = new Version('1'); + const version = new VersionTag('1'); beforeEach(() => { TestBed.configureTestingModule({ @@ -63,7 +62,6 @@ describe('ContentsService', () => { it(`should make post request to get contents using ${x.name}`, inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => { let contents: ContentsDto; - contentsService.getContents('my-app', 'my-schema', x.query).subscribe(result => { contents = result; }); @@ -85,20 +83,20 @@ describe('ContentsService', () => { statuses: [{ status: 'Draft', color: 'Gray', }], + _links: {}, }); - expect(contents!).toEqual({ + expect(contents!).toEqual(new ContentsDto({ items: [ createContent(12), createContent(13), ], total: 10, statuses: [ - { status: 'Draft', color: 'Gray' }, + new StatusInfoDto({ status: 'Draft', color: 'Gray' }), ], - canCreate: false, - canCreateAndPublish: false, - }); + _links: {}, + })); })); }); @@ -156,7 +154,6 @@ describe('ContentsService', () => { it('should make get request to get content', inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => { let content: ContentDto; - contentsService.getContent('my-app', 'my-schema', '1').subscribe(result => { content = result; }); @@ -174,7 +171,6 @@ describe('ContentsService', () => { it('should make get request to get raw content', inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => { let content: any; - contentsService.getRawContent('my-app', 'my-schema', '1').subscribe(result => { content = result; }); @@ -194,7 +190,6 @@ describe('ContentsService', () => { it('should make get request to get raw content with language', inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => { let content: any; - contentsService.getRawContent('my-app', 'my-schema', '1', 'en').subscribe(result => { content = result; }); @@ -216,7 +211,6 @@ describe('ContentsService', () => { const dto = {}; let content: ContentDto; - contentsService.postContent('my-app', 'my-schema', dto, true, 'my-id').subscribe(result => { content = result; }); @@ -236,12 +230,11 @@ describe('ContentsService', () => { const response = {}; let data: Versioned; - - contentsService.getVersionData('my-app', 'my-schema', 'content1', version).subscribe(result => { + contentsService.getVersionData('my-app', 'my-schema', 'content1', 42).subscribe(result => { data = result; }); - const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/content1/1'); + const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/content1/42'); expect(req.request.method).toEqual('GET'); expect(req.request.headers.get('If-Match')).toBeNull(); @@ -262,7 +255,6 @@ describe('ContentsService', () => { }; let content: ContentDto; - contentsService.putContent('my-app', resource, dto, version).subscribe(result => { content = result; }); @@ -288,7 +280,6 @@ describe('ContentsService', () => { }; let content: ContentDto; - contentsService.patchContent('my-app', resource, dto, version).subscribe(result => { content = result; }); @@ -312,7 +303,6 @@ describe('ContentsService', () => { }; let content: ContentDto; - contentsService.createVersion('my-app', resource, version).subscribe(result => { content = result; }); @@ -336,7 +326,6 @@ describe('ContentsService', () => { }; let content: ContentDto; - contentsService.deleteVersion('my-app', resource, version).subscribe(result => { content = result; }); @@ -360,7 +349,6 @@ describe('ContentsService', () => { }; let content: ContentDto; - contentsService.cancelStatus('my-app', resource, version).subscribe(result => { content = result; }); @@ -377,18 +365,20 @@ describe('ContentsService', () => { it('should make post request to for bulk update', inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => { - const dto: BulkUpdateDto = { - jobs: [{ - id: '123', - type: 'Delete', - }, { - id: '456', - type: 'Delete', - }], - }; + const dto = new BulkUpdateContentsDto({ + jobs: [ + new BulkUpdateContentsJobDto({ + id: '123', + type: 'Delete', + }), + new BulkUpdateContentsJobDto({ + id: '456', + type: 'Delete', + }), + ], + }); let results: ReadonlyArray; - contentsService.bulkUpdate('my-app', 'my-schema', dto).subscribe(result => { results = result; }); @@ -399,9 +389,11 @@ describe('ContentsService', () => { expect(req.request.headers.get('If-Match')).toBeNull(); req.flush([{ - contentId: '123', + jobIndex: 0, + id: '123', }, { - contentId: '456', + jobIndex: 1, + id: '456', error: { statusCode: 400, message: 'Invalid', @@ -409,8 +401,8 @@ describe('ContentsService', () => { }]); expect(results!).toEqual([ - new BulkResultDto('123'), - new BulkResultDto('456', new ErrorDto(400, 'Invalid')), + new BulkResultDto({ jobIndex: 0, id: '123' }), + new BulkResultDto({ jobIndex: 1, id: '456', error: new ServerErrorDto({ statusCode: 400, message: 'Invalid' }) }), ]); })); @@ -419,27 +411,29 @@ describe('ContentsService', () => { return { id: `id${id}`, - status: `Status${id}`, - statusColor: 'black', - newStatus: `StatusNew${id}`, - newStatusColor: 'black', created: buildDate(id, 10), createdBy: `creator${id}`, + data: {}, + isDeleted: false, lastModified: buildDate(id, 20), lastModifiedBy: `modifier${id}`, + newStatus: `StatusNew${id}`, + newStatusColor: 'black', + referenceData: {}, + referenceFields: [], scheduleJob: { + id: '42', status: 'Draft', scheduledBy: `Scheduler${id}`, color: 'red', dueTime: buildDate(id, 30), }, - isDeleted: false, - data: {}, - schemaName: 'my-schema', + schemaId: key, schemaDisplayName: 'MySchema', - referenceData: {}, - referenceFields: [], - version: key, + schemaName: 'my-schema', + status: `Status${id}`, + statusColor: 'black', + version: id, _links: { update: { method: 'PUT', href: `/contents/id${id}` }, }, @@ -448,28 +442,37 @@ describe('ContentsService', () => { }); export function createContent(id: number, suffix = '') { - const links: ResourceLinks = { - update: { method: 'PUT', href: `/contents/id${id}` }, - }; - const key = `${id}${suffix}`; - return new ContentDto(links, - `id${id}`, - DateTime.parseISO(buildDate(id, 10)), `creator${id}`, - DateTime.parseISO(buildDate(id, 20)), `modifier${id}`, - new Version(key), - `Status${key}`, - 'black', - `StatusNew${key}`, - 'black', - new ScheduleDto('Draft', `Scheduler${id}`, 'red', DateTime.parseISO(buildDate(id, 30))), - false, - {}, - 'my-schema', - 'MySchema', - {}, - []); + return new ContentDto({ + id: `id${id}`, + created: DateTime.parseISO(buildDate(id, 10)), + createdBy: `creator${id}`, + data: {}, + isDeleted: false, + lastModified: DateTime.parseISO(buildDate(id, 20)), + lastModifiedBy: `modifier${id}`, + newStatus: `StatusNew${id}`, + newStatusColor: 'black', + referenceData: {}, + referenceFields: [], + scheduleJob: new ScheduleJobDto({ + id: '42', + status: 'Draft', + scheduledBy: `Scheduler${id}`, + color: 'red', + dueTime: DateTime.parseISO(buildDate(id, 30)), + }), + schemaId: key, + schemaDisplayName: 'MySchema', + schemaName: 'my-schema', + status: `Status${id}`, + statusColor: 'black', + version: id, + _links: { + update: new ResourceLinkDto({ method: 'PUT', href: `/contents/id${id}` }), + }, + }); } function buildDate(id: number, add = 0) { diff --git a/frontend/src/app/shared/services/contents.service.ts b/frontend/src/app/shared/services/contents.service.ts index 21ec733da..8ca7a1ddb 100644 --- a/frontend/src/app/shared/services/contents.service.ts +++ b/frontend/src/app/shared/services/contents.service.ts @@ -9,155 +9,12 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { ApiUrlConfig, DateTime, ErrorDto, hasAnyLink, HTTP, mapVersioned, pretifyError, Resource, ResourceLinks, StringHelper, Version, Versioned } from '@app/framework'; +import { ApiUrlConfig, HTTP, mapVersioned, pretifyError, Resource, StringHelper, Version, Versioned, VersionOrTag } from '@app/framework'; +import { BulkResultDto, BulkUpdateContentsDto, ContentDto, ContentsDto } from './../model'; import { Query, sanitize } from './query'; -import { parseField, RootFieldDto } from './schemas.service'; export type StatusInfo = Readonly<{ status: string; color: string }>; -export class ScheduleDto { - constructor( - public readonly status: string, - public readonly scheduledBy: string, - public readonly color: string, - public readonly dueTime: DateTime, - ) { - } -} - -export class ContentDto { - public readonly _links: ResourceLinks; - - public readonly statusUpdates: ReadonlyArray; - - public readonly canDelete: boolean; - public readonly canDraftDelete: boolean; - public readonly canDraftCreate: boolean; - public readonly canCancelStatus: boolean; - public readonly canUpdate: boolean; - - public get canPublish() { - return this.statusUpdates.find(x => x.status === 'Published'); - } - - constructor(links: ResourceLinks, - public readonly id: string, - public readonly created: DateTime, - public readonly createdBy: string, - public readonly lastModified: DateTime, - public readonly lastModifiedBy: string, - public readonly version: Version, - public readonly status: string, - public readonly statusColor: string, - public readonly newStatus: string | undefined, - public readonly newStatusColor: string | undefined, - public readonly scheduleJob: ScheduleDto | null | undefined, - public readonly isDeleted: boolean, - public readonly data: ContentData, - public readonly schemaName: string, - public readonly schemaDisplayName: string, - public readonly referenceData: ContentReferences, - public readonly referenceFields: ReadonlyArray, - ) { - this._links = links; - - this.canDelete = hasAnyLink(links, 'delete'); - this.canDraftCreate = hasAnyLink(links, 'draft/create'); - this.canDraftDelete = hasAnyLink(links, 'draft/delete'); - this.canCancelStatus = hasAnyLink(links, 'cancel'); - this.canUpdate = hasAnyLink(links, 'update'); - - const updates: StatusInfo[] = []; - - for (const [key, link] of Object.entries(links)) { - if (key.startsWith('status/')) { - updates.push({ status: key.substring(7), color: link.metadata! }); - } - } - - this.statusUpdates = updates; - } -} - -export class BulkResultDto { - constructor( - public readonly contentId: string, - public readonly error?: ErrorDto, - ) { - } -} - -export type ContentsDto = Readonly<{ - // The list of content items. - items: ReadonlyArray; - - // The total number of content items. - total: number; - - // The statuses. - statuses: ReadonlyArray; - - // True, if the user has permissions to create a content item. - canCreate?: boolean; - - // True, if the user has permissions to create and publish a content item. - canCreateAndPublish?: boolean; -}>; - -export type ContentReferencesValue = Readonly<{ - // The references by partition. - [partition: string]: any; -}> | string; - -export type ContentReferences = Readonly<{ - // The reference values by field name. - [fieldName: string ]: ContentFieldData; -}>; - -export type ContentFieldData = Readonly<{ - // The data by partition. - [partition: string]: T; -}>; - -export type ContentData = Readonly<{ - // The content data by field name. - [fieldName: string ]: ContentFieldData; -}>; - -export type BulkStatusDto = Readonly<{ -}>; - -export type BulkUpdateDto = Readonly<{ - // The list of bulk update jobs. - jobs: ReadonlyArray; - - // True, if scripts should not be executed. - doNotScript?: boolean; - - // True, if referrers should be checked. - checkReferrers?: boolean; -}>; - -export type BulkUpdateJobDto = Readonly<{ - // The ID of the content to update. - id: string; - - // The type of the bulk update job. - type: 'Upsert' | 'ChangeStatus' | 'Delete' | 'Validate'; - - // The schema of the content item. - schema?: string; - - // The new status. - status?: string; - - // The due time of the new status. - dueTime?: string | null; - - // The expected version of the content. - expectedVersion?: number; -}>; - export type ContentsQuery = Readonly<{ // True, to not return the total number of items. noTotal?: boolean; @@ -222,7 +79,7 @@ export class ContentsService { return this.http.post(url, body, buildHeaders(q)).pipe( map(body => { - return parseContents(body); + return ContentsDto.fromJSON(body); }), pretifyError('i18n:contents.loadFailed')); } @@ -234,7 +91,7 @@ export class ContentsService { return this.http.post(url, body, buildHeaders(q)).pipe( map(body => { - return parseContents(body); + return ContentsDto.fromJSON(body); }), pretifyError('i18n:contents.loadFailed')); } @@ -246,7 +103,7 @@ export class ContentsService { return this.http.get(url, buildHeaders(q)).pipe( map(body => { - return parseContents(body); + return ContentsDto.fromJSON(body); }), pretifyError('i18n:contents.loadFailed')); } @@ -258,7 +115,7 @@ export class ContentsService { return this.http.get(url, buildHeaders(q)).pipe( map(body => { - return parseContents(body); + return ContentsDto.fromJSON(body); }), pretifyError('i18n:contents.loadFailed')); } @@ -268,7 +125,7 @@ export class ContentsService { return HTTP.getVersioned(this.http, url).pipe( map(({ payload }) => { - return parseContent(payload.body); + return ContentDto.fromJSON(payload.body); }), pretifyError('i18n:contents.loadContentFailed')); } @@ -291,7 +148,7 @@ export class ContentsService { } public getVersionData(appName: string, schemaName: string, id: string, version: Version): Observable> { - const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/${version.value}`); + const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/${version}`); return HTTP.getVersioned(this.http, url).pipe( mapVersioned(({ body }) => { @@ -305,77 +162,77 @@ export class ContentsService { return HTTP.postVersioned(this.http, url, data).pipe( map(({ payload }) => { - return parseContent(payload.body); + return ContentDto.fromJSON(payload.body); }), pretifyError('i18n:contents.createFailed')); } - public putContent(appName: string, resource: Resource, dto: any, version: Version): Observable { + public putContent(appName: string, resource: Resource, dto: any, version: VersionOrTag): Observable { const link = resource._links['update']; const url = this.apiUrl.buildUrl(link.href); return HTTP.putVersioned(this.http, url, dto, version).pipe( map(({ payload }) => { - return parseContent(payload.body); + return ContentDto.fromJSON(payload.body); }), pretifyError('i18n:contents.updateFailed')); } - public patchContent(appName: string, resource: Resource, dto: any, version: Version): Observable { + public patchContent(appName: string, resource: Resource, dto: any, version: VersionOrTag): Observable { const link = resource._links['patch']; const url = this.apiUrl.buildUrl(link.href); return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe( map(({ payload }) => { - return parseContent(payload.body); + return ContentDto.fromJSON(payload.body); }), pretifyError('i18n:contents.updateFailed')); } - public createVersion(appName: string, resource: Resource, version: Version): Observable { + public createVersion(appName: string, resource: Resource, version: VersionOrTag): Observable { const link = resource._links['draft/create']; const url = this.apiUrl.buildUrl(link.href); return HTTP.requestVersioned(this.http, link.method, url, version, {}).pipe( map(({ payload }) => { - return parseContent(payload.body); + return ContentDto.fromJSON(payload.body); }), pretifyError('i18n:contents.loadVersionFailed')); } - public cancelStatus(appName: string, resource: Resource, version: Version): Observable { + public cancelStatus(appName: string, resource: Resource, version: VersionOrTag): Observable { const link = resource._links['cancel']; const url = this.apiUrl.buildUrl(link.href); return HTTP.requestVersioned(this.http, link.method, url, version).pipe( map(({ payload }) => { - return parseContent(payload.body); + return ContentDto.fromJSON(payload.body); }), pretifyError('i18n:contents.updateFailed')); } - public deleteVersion(appName: string, resource: Resource, version: Version): Observable { + public deleteVersion(appName: string, resource: Resource, version: VersionOrTag): Observable { const link = resource._links['draft/delete']; const url = this.apiUrl.buildUrl(link.href); return HTTP.requestVersioned(this.http, link.method, url, version, {}).pipe( map(({ payload }) => { - return parseContent(payload.body); + return ContentDto.fromJSON(payload.body); }), pretifyError('i18n:contents.deleteVersionFailed')); } - public bulkUpdate(appName: string, schemaName: string, dto: BulkUpdateDto): Observable> { + public bulkUpdate(appName: string, schemaName: string, dto: BulkUpdateContentsDto): Observable> { const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/bulk`); return this.http.post(url, dto).pipe( map(body => { - return body.map(x => new BulkResultDto(x.contentId, parseError(x.error))); + return body.map(BulkResultDto.fromJSON); }), pretifyError('i18n:contents.bulkFailed')); } @@ -443,57 +300,4 @@ function buildQuery(q?: ContentsByQuery): { q?: object; odata?: string } { return { q: sanitize(queryObj) }; } -} - -function parseContents(response: { items: any[]; total: number; statuses: any } & Resource): ContentsDto { - const { items: list, total, statuses, _links } = response; - const items = list.map(parseContent); - - const canCreate = hasAnyLink(_links, 'create'); - const canCreateAndPublish = hasAnyLink(_links, 'create/publish'); - - return { items, total, statuses, canCreate, canCreateAndPublish }; -} - -function parseContent(response: any) { - return new ContentDto(response._links, - response.id, - DateTime.parseISO(response.created), response.createdBy, - DateTime.parseISO(response.lastModified), response.lastModifiedBy, - new Version(response.version.toString()), - response.status, - response.statusColor, - response.newStatus, - response.newStatusColor, - parseScheduleJob(response.scheduleJob), - response.isDeleted, - response.data, - response.schemaName, - response.schemaDisplayName, - response.referenceData, - response.referenceFields.map(parseField)); -} - -function parseScheduleJob(response: any) { - if (!response) { - return null; - } - - return new ScheduleDto( - response.status, - response.scheduledBy, - response.color, - DateTime.parseISO(response.dueTime)); -} - -function parseError(response: any) { - if (!response) { - return undefined; - } - - return new ErrorDto( - response.statusCode, - response.message, - response.errorCode, - response.details); -} +} \ No newline at end of file diff --git a/frontend/src/app/shared/services/contributors.service.spec.ts b/frontend/src/app/shared/services/contributors.service.spec.ts index f95023776..d93fee4e1 100644 --- a/frontend/src/app/shared/services/contributors.service.spec.ts +++ b/frontend/src/app/shared/services/contributors.service.spec.ts @@ -8,10 +8,11 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { ApiUrlConfig, ContributorDto, ContributorsDto, ContributorsPayload, ContributorsService, Resource, ResourceLinks, Version } from '@app/shared/internal'; +import { ApiUrlConfig, ContributorDto, ContributorsDto, ContributorsService, Resource, Versioned, VersionTag } from '@app/shared/internal'; +import { AssignContributorDto, ContributorsMetadataDto, ResourceLinkDto } from '../model'; describe('ContributorsService', () => { - const version = new Version('1'); + const version = new VersionTag('1'); beforeEach(() => { TestBed.configureTestingModule({ @@ -31,8 +32,7 @@ describe('ContributorsService', () => { it('should make get request to get app contributors', inject([ContributorsService, HttpTestingController], (contributorsService: ContributorsService, httpMock: HttpTestingController) => { - let contributors: ContributorsDto; - + let contributors: Versioned; contributorsService.getContributors('my-app').subscribe(result => { contributors = result; }); @@ -48,15 +48,14 @@ describe('ContributorsService', () => { }, }); - expect(contributors!).toEqual({ payload: createContributors(1, 2, 3), version: new Version('2') }); + expect(contributors!).toEqual({ payload: createContributors(1, 2, 3), version: new VersionTag('2') }); })); it('should make post request to assign contributor', inject([ContributorsService, HttpTestingController], (contributorsService: ContributorsService, httpMock: HttpTestingController) => { - const dto = { contributorId: '123', role: 'Owner' }; - - let contributors: ContributorsDto; + const dto = new AssignContributorDto({ contributorId: '123', role: 'Owner' }); + let contributors: Versioned; contributorsService.postContributor('my-app', dto, version).subscribe(result => { contributors = result; }); @@ -72,7 +71,7 @@ describe('ContributorsService', () => { }, }); - expect(contributors!).toEqual({ payload: createContributors(1, 2, 3), version: new Version('2') }); + expect(contributors!).toEqual({ payload: createContributors(1, 2, 3), version: new VersionTag('2') }); })); it('should make delete request to remove contributor', @@ -83,8 +82,7 @@ describe('ContributorsService', () => { }, }; - let contributors: ContributorsDto; - + let contributors: Versioned; contributorsService.deleteContributor('my-app', resource, version).subscribe(result => { contributors = result; }); @@ -100,7 +98,7 @@ describe('ContributorsService', () => { }, }); - expect(contributors!).toEqual({ payload: createContributors(1, 2, 3), version: new Version('2') }); + expect(contributors!).toEqual({ payload: createContributors(1, 2, 3), version: new VersionTag('2') }); })); function contributorsResponse(...ids: number[]) { @@ -125,19 +123,27 @@ describe('ContributorsService', () => { } }); -export function createContributors(...ids: ReadonlyArray): ContributorsPayload { - return { - maxContributors: ids.length * 13, +export function createContributors(...ids: ReadonlyArray) { + return new ContributorsDto({ items: ids.map(createContributor), - isInvited: false, - canCreate: true, - }; + maxContributors: ids.length * 13, + _links: { + create: new ResourceLinkDto({ method: 'POST', href: '/contributors' }), + }, + _meta: new ContributorsMetadataDto({ + isInvited: 'true', + }), + }); } export function createContributor(id: number) { - const links: ResourceLinks = { - update: { method: 'PUT', href: `/contributors/id${id}` }, - }; - - return new ContributorDto(links, `id${id}`, `name${id}`, `mail${id}@squidex.io`, id % 2 === 0 ? 'Owner' : 'Developer'); + return new ContributorDto({ + contributorId: `id${id}`, + contributorName: `name${id}`, + contributorEmail: `mail${id}@squidex.io`, + role: id % 2 === 0 ? 'Owner' : 'Developer', + _links: { + update: new ResourceLinkDto({ method: 'PUT', href: `/contributors/id${id}` }), + }, + }); } diff --git a/frontend/src/app/shared/services/contributors.service.ts b/frontend/src/app/shared/services/contributors.service.ts index e620e68f0..a4d43ff57 100644 --- a/frontend/src/app/shared/services/contributors.service.ts +++ b/frontend/src/app/shared/services/contributors.service.ts @@ -8,10 +8,8 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { ApiUrlConfig, HTTP, mapVersioned, pretifyError, Resource, Version } from '@app/framework'; -import { AssignContributorDto, ContributorsDto, parseContributors } from './shared'; - -export * from './shared'; +import { ApiUrlConfig, HTTP, mapVersioned, pretifyError, Resource, Versioned, VersionOrTag } from '@app/framework'; +import { AssignContributorDto, ContributorsDto } from './../model'; @Injectable({ providedIn: 'root', @@ -23,34 +21,34 @@ export class ContributorsService { ) { } - public getContributors(appName: string): Observable { + public getContributors(appName: string): Observable> { const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors`); return HTTP.getVersioned(this.http, url).pipe( mapVersioned(({ body }) => { - return parseContributors(body); + return ContributorsDto.fromJSON(body); }), pretifyError('i18n:contributors.loadFailed')); } - public postContributor(appName: string, dto: AssignContributorDto, version: Version): Observable { + public postContributor(appName: string, dto: AssignContributorDto, version: VersionOrTag): Observable> { const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors`); - return HTTP.postVersioned(this.http, url, dto, version).pipe( + return HTTP.postVersioned(this.http, url, dto.toJSON(), version).pipe( mapVersioned(({ body }) => { - return parseContributors(body); + return ContributorsDto.fromJSON(body); }), pretifyError('i18n:contributors.addFailed')); } - public deleteContributor(appName: string, resource: Resource, version: Version): Observable { + public deleteContributor(appName: string, resource: Resource, version: VersionOrTag): Observable> { const link = resource._links['delete']; const url = this.apiUrl.buildUrl(link.href); return HTTP.requestVersioned(this.http, link.method, url, version).pipe( mapVersioned(({ body }) => { - return parseContributors(body); + return ContributorsDto.fromJSON(body); }), pretifyError('i18n:contributors.deleteFailed')); } diff --git a/frontend/src/app/shared/services/history.service.spec.ts b/frontend/src/app/shared/services/history.service.spec.ts index 6837c33e0..e6d2cc68b 100644 --- a/frontend/src/app/shared/services/history.service.spec.ts +++ b/frontend/src/app/shared/services/history.service.spec.ts @@ -10,7 +10,7 @@ import { HttpTestingController, provideHttpClientTesting } from '@angular/common import { inject, TestBed } from '@angular/core/testing'; import { firstValueFrom, of } from 'rxjs'; import { IMock, Mock } from 'typemoq'; -import { ApiUrlConfig, DateTime, formatHistoryMessage, HistoryEventDto, HistoryService, UsersProviderService, Version } from '@app/shared/internal'; +import { ApiUrlConfig, DateTime, formatHistoryMessage, HistoryEventDto, HistoryService, UserDto, UsersProviderService } from '@app/shared/internal'; describe('formatHistoryMessage', () => { let userProvider: IMock; @@ -57,7 +57,7 @@ describe('formatHistoryMessage', () => { it('should embed user ref with subject', async () => { userProvider.setup(x => x.getUser('1', null)) - .returns(() => of({ id: '1', displayName: 'User1' })); + .returns(() => of(new UserDto({ id: '1', displayName: 'User1' } as any))); const message = await firstValueFrom(formatHistoryMessage('{user:subject:1}', userProvider.object)); @@ -66,7 +66,7 @@ describe('formatHistoryMessage', () => { it('should embed user ref with id', async () => { userProvider.setup(x => x.getUser('1', null)) - .returns(() => of({ id: '1', displayName: 'User1' })); + .returns(() => of(new UserDto({ id: '1', displayName: 'User1' } as any))); const message = await firstValueFrom(formatHistoryMessage('{user:1}', userProvider.object)); @@ -126,32 +126,46 @@ describe('HistoryService', () => { expect(events!).toEqual(createHistory()); })); + + function historyResponse() { + return [ + { + actor: 'User1', + created: '2016-12-12T10:10', + eventId: '1', + eventType: 'Type 1', + message: 'Message 1', + version: 2, + }, + { + actor: 'User2', + created: '2016-12-13T10:10', + eventId: '2', + eventType: 'Type 2', + message: 'Message 2', + version: 3, + }, + ]; + } }); export function createHistory() { return [ - new HistoryEventDto('1', 'User1', 'Type 1', 'Message 1', DateTime.parseISO('2016-12-12T10:10Z'), new Version('2')), - new HistoryEventDto('2', 'User2', 'Type 2', 'Message 2', DateTime.parseISO('2016-12-13T10:10Z'), new Version('3')), - ]; -} - -function historyResponse() { - return [ - { + new HistoryEventDto({ actor: 'User1', + created: DateTime.parseISO('2016-12-12T10:10'), eventId: '1', eventType: 'Type 1', message: 'Message 1', version: 2, - created: '2016-12-12T10:10', - }, - { + }), + new HistoryEventDto({ actor: 'User2', + created: DateTime.parseISO('2016-12-13T10:10'), eventId: '2', eventType: 'Type 2', message: 'Message 2', version: 3, - created: '2016-12-13T10:10', - }, + }), ]; -} +} \ No newline at end of file diff --git a/frontend/src/app/shared/services/history.service.ts b/frontend/src/app/shared/services/history.service.ts index 8522e6a57..a8babb35e 100644 --- a/frontend/src/app/shared/services/history.service.ts +++ b/frontend/src/app/shared/services/history.service.ts @@ -9,21 +9,10 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { firstValueFrom, from, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { ApiUrlConfig, DateTime, escapeHTML, pretifyError, StringHelper, Version } from '@app/framework'; +import { ApiUrlConfig, escapeHTML, pretifyError, StringHelper } from '@app/framework'; +import { HistoryEventDto } from './../model'; import { UsersProviderService } from './users-provider.service'; -export class HistoryEventDto { - constructor( - public readonly eventId: string, - public readonly actor: string, - public readonly eventType: string, - public readonly message: string, - public readonly created: DateTime, - public readonly version: Version, - ) { - } -} - export function formatHistoryMessage(message: string, users: UsersProviderService): Observable { async function getUserName(id: string): Promise { const user = await firstValueFrom(users.getUser(id, null)); @@ -91,7 +80,7 @@ export class HistoryService { return this.http.get(url, options).pipe( map(body => { - return parseHistoryEvents(body); + return body.map(HistoryEventDto.fromJSON); }), pretifyError('i18n:history.loadFailed')); } @@ -107,22 +96,8 @@ export class HistoryService { return this.http.get(url, options).pipe( map(body => { - return parseHistoryEvents(body); + return body.map(HistoryEventDto.fromJSON); }), pretifyError('i18n:history.loadFailed')); } -} - -function parseHistoryEvents(response: any[]) { - return response.map(parseHistoryEvent); -} - -function parseHistoryEvent(response: any) { - return new HistoryEventDto( - response.eventId, - response.actor, - response.eventType, - response.message, - DateTime.parseISO(response.created), - new Version(response.version.toString())); -} +} \ No newline at end of file diff --git a/frontend/src/app/shared/services/indexes.service.spec.ts b/frontend/src/app/shared/services/indexes.service.spec.ts index ea22d1514..68f66afe9 100644 --- a/frontend/src/app/shared/services/indexes.service.spec.ts +++ b/frontend/src/app/shared/services/indexes.service.spec.ts @@ -8,7 +8,8 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { ApiUrlConfig, IndexDto, IndexesDto, IndexesService, Resource, ResourceLinks } from '@app/shared/internal'; +import { ApiUrlConfig, IndexDto, IndexesDto, IndexesService, IndexFieldDto, Resource } from '@app/shared/internal'; +import { CreateIndexDto, ResourceLinkDto } from '../model'; describe('IndexesService', () => { beforeEach(() => { @@ -30,7 +31,6 @@ describe('IndexesService', () => { it('should make get request to get indexes', inject([IndexesService, HttpTestingController], (indexesService: IndexesService, httpMock: HttpTestingController) => { let indexes: IndexesDto; - indexesService.getIndexes('my-app', 'my-schema').subscribe(result => { indexes = result; }); @@ -45,20 +45,21 @@ describe('IndexesService', () => { indexResponse(12), indexResponse(13), ], + _links: {}, }); - expect(indexes!).toEqual({ + expect(indexes!).toEqual(new IndexesDto({ items: [ createIndex(12), createIndex(13), ], - canCreate: false, - }); + _links: {}, + })); })); it('should make post request to create index', inject([IndexesService, HttpTestingController], (indexesService: IndexesService, httpMock: HttpTestingController) => { - const request = { fields: [] }; + const request = new CreateIndexDto({ fields: [] }); indexesService.postIndex('my-app', 'my-schema', request).subscribe(); @@ -103,14 +104,14 @@ describe('IndexesService', () => { }); export function createIndex(id: number) { - const links: ResourceLinks = { - download: { method: 'GET', href: '/api/indexes/1' }, - }; - - return new IndexDto(links, - `index${id}`, - [ - { name: `field${id}_asc`, order: 'Ascending' }, - { name: `field${id}_desc`, order: 'Descending' }, - ]); + return new IndexDto({ + name: `index${id}`, + fields: [ + new IndexFieldDto({ name: `field${id}_asc`, order: 'Ascending' }), + new IndexFieldDto({ name: `field${id}_desc`, order: 'Descending' }), + ], + _links: { + download: new ResourceLinkDto({ method: 'GET', href: '/api/indexes/1' }), + }, + }); } \ No newline at end of file diff --git a/frontend/src/app/shared/services/indexes.service.ts b/frontend/src/app/shared/services/indexes.service.ts index 39ff38620..477632865 100644 --- a/frontend/src/app/shared/services/indexes.service.ts +++ b/frontend/src/app/shared/services/indexes.service.ts @@ -8,37 +8,8 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { map, Observable } from 'rxjs'; -import { ApiUrlConfig, hasAnyLink, pretifyError, Resource, ResourceLinks } from '@app/framework'; - -export type IndexField = { name: string; order: 'Ascending' | 'Descending' }; - -export class IndexDto { - public readonly _links: ResourceLinks; - - public readonly canDelete: boolean; - - constructor(links: ResourceLinks, - public readonly name: string, - public readonly fields: ReadonlyArray, - ) { - this._links = links; - - this.canDelete = hasAnyLink(links, 'delete'); - } -} - -export type IndexesDto = Readonly<{ - // The indexes. - items: ReadonlyArray; - - // The if the user can create a new index. - canCreate?: boolean; -}>; - -export type CreateIndexDto = Readonly<{ - // The index fields. - fields: IndexField[]; -}>; +import { ApiUrlConfig, pretifyError, Resource } from '@app/framework'; +import { CreateIndexDto, IndexesDto } from './../model'; @Injectable({ providedIn: 'root', @@ -55,7 +26,7 @@ export class IndexesService { return this.http.get(url).pipe( map(body => { - return parseIndexes(body as any); + return IndexesDto.fromJSON(body as any); }), pretifyError('i18n:schemas.indexes.loadFailed')); } @@ -63,7 +34,7 @@ export class IndexesService { public postIndex(appName: string, schemaName: string, dto: CreateIndexDto): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/indexes`); - return this.http.post(url, dto).pipe( + return this.http.post(url, dto.toJSON()).pipe( pretifyError('i18n:schemas.indexes.createFailed')); } @@ -77,17 +48,4 @@ export class IndexesService { } } -function parseIndexes(response: { items: any[] } & Resource): IndexesDto { - const { items: list, _links } = response; - const items = list.map(parseIndex); - - const canCreate = hasAnyLink(_links, 'create'); - - return { items, canCreate }; -} - -function parseIndex(response: any) { - return new IndexDto(response._links, - response.name, - response.fields); -} +export { CreateIndexDto }; diff --git a/frontend/src/app/shared/services/jobs.service.spec.ts b/frontend/src/app/shared/services/jobs.service.spec.ts index 385695a9d..2a3b4a6fd 100644 --- a/frontend/src/app/shared/services/jobs.service.spec.ts +++ b/frontend/src/app/shared/services/jobs.service.spec.ts @@ -8,7 +8,8 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { ApiUrlConfig, DateTime, JobDto, JobLogMessageDto, JobsDto, JobsService, Resource, ResourceLinks, RestoreDto } from '@app/shared/internal'; +import { ApiUrlConfig, DateTime, JobDto, JobLogMessageDto, JobsDto, JobsService, Resource } from '@app/shared/internal'; +import { ResourceLinkDto, RestoreJobDto, RestoreRequestDto } from '../model'; describe('JobsService', () => { beforeEach(() => { @@ -30,7 +31,6 @@ describe('JobsService', () => { it('should make get request to get jobs', inject([JobsService, HttpTestingController], (jobsService: JobsService, httpMock: HttpTestingController) => { let jobs: JobsDto; - jobsService.getJobs('my-app').subscribe(result => { jobs = result; }); @@ -45,21 +45,21 @@ describe('JobsService', () => { jobResponse(12), jobResponse(13), ], + _links: {}, }); - expect(jobs!).toEqual({ + expect(jobs!).toEqual(new JobsDto({ items: [ createJob(12), createJob(13), ], - canCreateBackup: false, - }); + _links: {}, + })); })); it('should make get request to get restore', inject([JobsService, HttpTestingController], (jobsService: JobsService, httpMock: HttpTestingController) => { - let restore: RestoreDto; - + let restore: RestoreJobDto; jobsService.getRestore().subscribe(result => { restore = result!; }); @@ -80,21 +80,21 @@ describe('JobsService', () => { ], }); - expect(restore!).toEqual( - new RestoreDto('http://url', - DateTime.parseISO('2017-02-03'), - DateTime.parseISO('2017-02-04'), - 'Failed', - [ - 'log1', - 'log2', - ])); + expect(restore!).toEqual(new RestoreJobDto({ + url: 'http://url', + started: DateTime.parseISO('2017-02-03'), + stopped: DateTime.parseISO('2017-02-04'), + status: 'Failed', + log: [ + 'log1', + 'log2', + ], + })); })); it('should return null if get restore returns 404', inject([JobsService, HttpTestingController], (jobsService: JobsService, httpMock: HttpTestingController) => { - let restore: RestoreDto | null; - + let restore: RestoreJobDto | null; jobsService.getRestore().subscribe(result => { restore = result; }); @@ -143,7 +143,7 @@ describe('JobsService', () => { it('should make post request to start restore', inject([JobsService, HttpTestingController], (jobsService: JobsService, httpMock: HttpTestingController) => { - const dto = { url: 'http://url' }; + const dto = new RestoreRequestDto({ url: 'http://url' }); jobsService.postRestore(dto).subscribe(); @@ -176,21 +176,25 @@ describe('JobsService', () => { function jobResponse(id: number) { return { id: `id${id}`, + canDownload: false, + description: `description${id}`, + log: [ + { + timestamp: buildDate(id, 30), + message: `log1_${id}`, + }, + { + timestamp: buildDate(id, 40), + message: `log2_${id}`, + }, + ], + status: id % 2 === 0 ? 'Success' : 'Failed', started: buildDate(id, 10), stopped: buildDate(id, 20), taskName: `task${id}`, taskArguments: { [`arg${id}`]: '42', }, - description: `description${id}`, - log: [{ - timestamp: buildDate(id, 30), - message: `log1_${id}`, - }, { - timestamp: buildDate(id, 40), - message: `log2_${id}`, - }], - status: id % 2 === 0 ? 'Success' : 'Failed', _links: { download: { method: 'GET', href: '/api/jobs/1' }, }, @@ -199,24 +203,31 @@ describe('JobsService', () => { }); export function createJob(id: number) { - const links: ResourceLinks = { - download: { method: 'GET', href: '/api/jobs/1' }, - }; - - return new JobDto(links, - `id${id}`, - DateTime.parseISO(buildDate(id, 10)), - DateTime.parseISO(buildDate(id, 20)), - `task${id}`, - { + return new JobDto({ + id: `id${id}`, + canDownload: false, + description: `description${id}`, + log: [ + new JobLogMessageDto({ + timestamp: DateTime.parseISO(buildDate(id, 30)), + message: `log1_${id}`, + }), + new JobLogMessageDto({ + timestamp: DateTime.parseISO(buildDate(id, 40)), + message: `log2_${id}`, + }), + ], + started: DateTime.parseISO(buildDate(id, 10)), + status: id % 2 === 0 ? 'Success' : 'Failed' as any, + stopped: DateTime.parseISO(buildDate(id, 20)), + taskName: `task${id}`, + taskArguments: { [`arg${id}`]: '42', }, - `description${id}`, - [ - new JobLogMessageDto(DateTime.parseISO(buildDate(id, 30)), `log1_${id}`), - new JobLogMessageDto(DateTime.parseISO(buildDate(id, 40)), `log2_${id}`), - ], - id % 2 === 0 ? 'Success' : 'Failed'); + _links: { + download: new ResourceLinkDto({ method: 'GET', href: '/api/jobs/1' }), + }, + }); } function buildDate(id: number, add = 0) { diff --git a/frontend/src/app/shared/services/jobs.service.ts b/frontend/src/app/shared/services/jobs.service.ts index a36baca89..695384605 100644 --- a/frontend/src/app/shared/services/jobs.service.ts +++ b/frontend/src/app/shared/services/jobs.service.ts @@ -9,73 +9,9 @@ import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of, throwError } from 'rxjs'; import { catchError, map } from 'rxjs/operators'; -import { ApiUrlConfig, DateTime, hasAnyLink, pretifyError, Resource, ResourceLinks, Types } from '@app/framework'; +import { ApiUrlConfig, pretifyError, Resource, Types } from '@app/framework'; +import { JobsDto, RestoreJobDto, RestoreRequestDto } from './../model'; -export class JobDto { - public readonly _links: ResourceLinks; - - public readonly canDelete: boolean; - public readonly canDownload: boolean; - - public readonly downloadUrl?: string; - - public get isFailed() { - return this.status === 'Failed'; - } - - constructor(links: ResourceLinks, - public readonly id: string, - public readonly started: DateTime, - public readonly stopped: DateTime | null, - public readonly taskName: string, - public readonly taskArguments: Record, - public readonly description: string, - public readonly log: ReadonlyArray, - public readonly status: 'Started' | 'Failed' | 'Success' | 'Completed' | 'Pending', - ) { - this._links = links; - - this.canDelete = hasAnyLink(links, 'delete'); - this.canDownload = hasAnyLink(links, 'download'); - - this.downloadUrl = links['download']?.href; - } -} - -export class JobLogMessageDto { - constructor( - public readonly timestamp: DateTime, - public readonly message: string, - ) { - } -} - -export class RestoreDto { - constructor( - public readonly url: string, - public readonly started: DateTime, - public readonly stopped: DateTime | null, - public readonly status: string, - public readonly log: ReadonlyArray, - ) { - } -} - -export type JobsDto = Readonly<{ - // The list of jobs. - items: ReadonlyArray; - - // True, if the user has permissions to create a backup. - canCreateBackup?: boolean; -}>; - -export type StartRestoreDto = Readonly<{ - // The url of the backup file. - url: string; - - // The optional app name tro use. - newAppName?: string; -}>; @Injectable({ providedIn: 'root', @@ -92,19 +28,17 @@ export class JobsService { return this.http.get<{ items: any[]; _links: {} } & Resource>(url).pipe( map(body => { - return parseJobs(body); + return JobsDto.fromJSON(body); }), pretifyError('i18n:jobs.loadFailed')); } - public getRestore(): Observable { + public getRestore(): Observable { const url = this.apiUrl.buildUrl('api/apps/restore'); return this.http.get(url).pipe( map(body => { - const restore = parseRestore(body); - - return restore; + return RestoreJobDto.fromJSON(body); }), catchError(error => { if (Types.is(error, HttpErrorResponse) && error.status === 404) { @@ -123,10 +57,10 @@ export class JobsService { pretifyError('i18n:jobs.backupFailed')); } - public postRestore(dto: StartRestoreDto): Observable { + public postRestore(dto: RestoreRequestDto): Observable { const url = this.apiUrl.buildUrl('api/apps/restore'); - return this.http.post(url, dto).pipe( + return this.http.post(url, dto.toJSON()).pipe( pretifyError('i18n:jobs.restoreFailed')); } @@ -138,36 +72,4 @@ export class JobsService { return this.http.request(link.method, url).pipe( pretifyError('i18n:jobs.deleteFailed')); } -} - -function parseJobs(response: { items: any[] } & Resource): JobsDto { - const { items: list, _links } = response; - const items = list.map(parseJob); - - const canCreateBackup = hasAnyLink(_links, 'create/backups'); - - return { items, canCreateBackup }; -} - -function parseRestore(response: any) { - return new RestoreDto( - response.url, - DateTime.parseISO(response.started), - response.stopped ? DateTime.parseISO(response.stopped) : null, - response.status, - response.log); -} - -function parseJob(response: any & Resource) { - const log: any[] = response.log; - - return new JobDto(response._links, - response.id, - DateTime.parseISO(response.started), - response.stopped ? DateTime.parseISO(response.stopped) : null, - response.taskName, - response.taskArguments, - response.description, - log.map(x => new JobLogMessageDto(DateTime.parseISO(x.timestamp), x.message)), - response.status); -} +} \ No newline at end of file diff --git a/frontend/src/app/shared/services/languages.service.spec.ts b/frontend/src/app/shared/services/languages.service.spec.ts index e37ae3791..ae5727e10 100644 --- a/frontend/src/app/shared/services/languages.service.spec.ts +++ b/frontend/src/app/shared/services/languages.service.spec.ts @@ -51,10 +51,15 @@ describe('LanguageService', () => { }, ]); - expect(languages!).toEqual( - [ - new LanguageDto('de', 'German'), - new LanguageDto('en', 'English'), - ]); + expect(languages!).toEqual([ + new LanguageDto({ + iso2Code: 'de', + englishName: 'German', + }), + new LanguageDto({ + iso2Code: 'en', + englishName: 'English', + }), + ]); })); }); diff --git a/frontend/src/app/shared/services/languages.service.ts b/frontend/src/app/shared/services/languages.service.ts index 4ff2d7c31..91ac3b46c 100644 --- a/frontend/src/app/shared/services/languages.service.ts +++ b/frontend/src/app/shared/services/languages.service.ts @@ -10,14 +10,7 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { ApiUrlConfig, pretifyError } from '@app/framework'; - -export class LanguageDto { - constructor( - public readonly iso2Code: string, - public readonly englishName: string, - ) { - } -} +import { LanguageDto } from './../model'; @Injectable({ providedIn: 'root', @@ -34,18 +27,8 @@ export class LanguagesService { return this.http.get(url).pipe( map(body => { - return parseLanguages(body); + return body.map(LanguageDto.fromJSON); }), pretifyError('i18n:languages.loadFailed')); } } - -function parseLanguages(response: any[]) { - return response.map(parseLanguage); -} - -function parseLanguage(response: any) { - return new LanguageDto( - response.iso2Code, - response.englishName); -} diff --git a/frontend/src/app/shared/services/news.service.spec.ts b/frontend/src/app/shared/services/news.service.spec.ts index 856d00565..fbcc4d829 100644 --- a/frontend/src/app/shared/services/news.service.spec.ts +++ b/frontend/src/app/shared/services/news.service.spec.ts @@ -8,7 +8,7 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { ApiUrlConfig, FeaturesDto, NewsService } from '@app/shared/internal'; +import { ApiUrlConfig, FeatureDto, FeaturesDto, NewsService } from '@app/shared/internal'; describe('NewsService', () => { beforeEach(() => { @@ -41,25 +41,30 @@ describe('NewsService', () => { expect(req.request.headers.get('If-Match')).toBeNull(); req.flush({ - features: [{ - name: 'Feature1', - text: 'Feature Text1', - }, { - name: 'Feature2', - text: 'Feature Text2', - }], + features: [ + { + name: 'Feature1', + text: 'Feature Text1', + }, { + name: 'Feature2', + text: 'Feature Text2', + }, + ], version: 13, }); - expect(features!).toEqual({ - features: [{ - name: 'Feature1', - text: 'Feature Text1', - }, { - name: 'Feature2', - text: 'Feature Text2', - }], + expect(features!).toEqual(new FeaturesDto({ + features: [ + new FeatureDto({ + name: 'Feature1', + text: 'Feature Text1', + }), + new FeatureDto({ + name: 'Feature2', + text: 'Feature Text2', + }), + ], version: 13, - }); + })); })); }); diff --git a/frontend/src/app/shared/services/news.service.ts b/frontend/src/app/shared/services/news.service.ts index 6d7a2c233..5f88be449 100644 --- a/frontend/src/app/shared/services/news.service.ts +++ b/frontend/src/app/shared/services/news.service.ts @@ -7,24 +7,9 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; +import { map, Observable } from 'rxjs'; import { ApiUrlConfig, pretifyError, StringHelper } from '@app/framework'; - -export type FeatureDto = Readonly<{ - // The name of the feature. - name: string; - - // The feature description. - text: string; -}>; - -export type FeaturesDto = Readonly<{ - // The list of features. - features: ReadonlyArray; - - // The latest version. - version: number; -}>; +import { FeaturesDto } from '../model'; @Injectable({ providedIn: 'root', @@ -39,7 +24,10 @@ export class NewsService { public getFeatures(version: number): Observable { const url = this.apiUrl.buildUrl(`api/news/features${StringHelper.buildQuery({ version })}`); - return this.http.get(url).pipe( + return this.http.get(url).pipe( + map(body => { + return FeaturesDto.fromJSON(body); + }), pretifyError('i18n:features.loadFailed')); } } diff --git a/frontend/src/app/shared/services/plans.service.spec.ts b/frontend/src/app/shared/services/plans.service.spec.ts index f03e51ed8..7266603ad 100644 --- a/frontend/src/app/shared/services/plans.service.spec.ts +++ b/frontend/src/app/shared/services/plans.service.spec.ts @@ -8,10 +8,10 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { ApiUrlConfig, PlanChangedDto, PlanDto, PlansDto, PlansService, Version } from '@app/shared/internal'; +import { ApiUrlConfig, ChangePlanDto, PlanChangedDto, PlanDto, PlansDto, PlansService, ReferralInfoDto, Versioned, VersionTag } from '@app/shared/internal'; describe('PlansService', () => { - const version = new Version('1'); + const version = new VersionTag('1'); beforeEach(() => { TestBed.configureTestingModule({ @@ -31,8 +31,7 @@ describe('PlansService', () => { it('should make get request to get app plans', inject([PlansService, HttpTestingController], (plansService: PlansService, httpMock: HttpTestingController) => { - let plans: PlansDto; - + let plans: Versioned; plansService.getPlans('my-app').subscribe(result => { plans = result; }); @@ -43,7 +42,7 @@ describe('PlansService', () => { expect(req.request.headers.get('If-Match')).toBeNull(); req.flush({ - currentPlanId: '123', + currentPlanId: 'free', portalLink: 'link/to/portal', planOwner: '456', plans: [ @@ -74,9 +73,7 @@ describe('PlansService', () => { maxContributors: 6500, }, ], - referral: { - code: 'CODE', - }, + referral: { code: 'CODE', earned: '0', condition: 'None' }, locked: 'ManagedByTeam', }, { headers: { @@ -85,45 +82,50 @@ describe('PlansService', () => { }); expect(plans!).toEqual({ - payload: { - currentPlanId: '123', + payload: new PlansDto({ + currentPlanId: 'free', portalLink: 'link/to/portal', planOwner: '456', plans: [ - new PlanDto( - 'free', - 'Free', - '14 €', - 'Change for 14 € per month?', - 'free_yearly', - '120 €', - 'Change for 120 € per year?', - 128, 1000, 1500, 2500), - new PlanDto( - 'professional', - 'Professional', - '18 €', - 'Change for 18 € per month?', - 'professional_yearly', - '160 €', - 'Change for 160 € per year?', - 512, 4000, 5500, 6500), + new PlanDto({ + id: 'free', + name: 'Free', + costs: '14 €', + confirmText: 'Change for 14 € per month?', + yearlyId: 'free_yearly', + yearlyCosts: '120 €', + yearlyConfirmText: 'Change for 120 € per year?', + maxApiBytes: 128, + maxApiCalls: 1000, + maxAssetSize: 1500, + maxContributors: 2500, + }), + new PlanDto({ + id: 'professional', + name: 'Professional', + costs: '18 €', + confirmText: 'Change for 18 € per month?', + yearlyId: 'professional_yearly', + yearlyCosts: '160 €', + yearlyConfirmText: 'Change for 160 € per year?', + maxApiBytes: 512, + maxApiCalls: 4000, + maxAssetSize: 5500, + maxContributors: 6500, + }), ], - referral: { - code: 'CODE', - } as any, + referral: new ReferralInfoDto({ code: 'CODE', earned: '0', condition: 'None' }), locked: 'ManagedByTeam', - }, - version: new Version('2'), + }), + version: new VersionTag('2'), }); })); it('should make put request to change plan', inject([PlansService, HttpTestingController], (plansService: PlansService, httpMock: HttpTestingController) => { - const dto = { planId: 'enterprise' }; + const dto = new ChangePlanDto({ planId: 'enterprise' }); let planChanged: PlanChangedDto; - plansService.putPlan('my-app', dto, version).subscribe(result => { planChanged = result.payload; }); @@ -135,6 +137,6 @@ describe('PlansService', () => { expect(req.request.method).toEqual('PUT'); expect(req.request.headers.get('If-Match')).toBe(version.value); - expect(planChanged!).toEqual({ redirectUri: 'http://url' }); + expect(planChanged!).toEqual(new PlanChangedDto({ redirectUri: 'http://url' })); })); }); diff --git a/frontend/src/app/shared/services/plans.service.ts b/frontend/src/app/shared/services/plans.service.ts index ffb77599e..18c772cbd 100644 --- a/frontend/src/app/shared/services/plans.service.ts +++ b/frontend/src/app/shared/services/plans.service.ts @@ -8,10 +8,8 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { ApiUrlConfig, HTTP, mapVersioned, pretifyError, Version, Versioned } from '@app/framework'; -import { ChangePlanDto, parsePlans, PlanChangedDto, PlansDto } from './shared'; - -export * from './shared'; +import { ApiUrlConfig, HTTP, mapVersioned, pretifyError, Versioned, VersionOrTag } from '@app/framework'; +import { ChangePlanDto, PlanChangedDto, PlansDto } from '../model'; @Injectable({ providedIn: 'root', @@ -23,22 +21,22 @@ export class PlansService { ) { } - public getPlans(appName: string): Observable { + public getPlans(appName: string): Observable> { const url = this.apiUrl.buildUrl(`api/apps/${appName}/plans`); return HTTP.getVersioned(this.http, url).pipe( mapVersioned(({ body }) => { - return parsePlans(body); + return PlansDto.fromJSON(body); }), pretifyError('i18n:plans.loadFailed')); } - public putPlan(appName: string, dto: ChangePlanDto, version: Version): Observable> { + public putPlan(appName: string, dto: ChangePlanDto, version: VersionOrTag): Observable> { const url = this.apiUrl.buildUrl(`api/apps/${appName}/plan`); - return HTTP.putVersioned(this.http, url, dto, version).pipe( + return HTTP.putVersioned(this.http, url, dto.toJSON(), version).pipe( mapVersioned(({ body }) => { - return body; + return PlanChangedDto.fromJSON(body); }), pretifyError('i18n:plans.changeFailed')); } diff --git a/frontend/src/app/shared/services/roles.service.spec.ts b/frontend/src/app/shared/services/roles.service.spec.ts index 46e140fd7..d47cde141 100644 --- a/frontend/src/app/shared/services/roles.service.spec.ts +++ b/frontend/src/app/shared/services/roles.service.spec.ts @@ -8,10 +8,11 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { ApiUrlConfig, Resource, ResourceLinks, RoleDto, RolesDto, RolesPayload, RolesService, Version } from '@app/shared/internal'; +import { ApiUrlConfig, Resource, RoleDto, RolesDto, RolesService, Versioned, VersionTag } from '@app/shared/internal'; +import { AddRoleDto, ResourceLinkDto, UpdateRoleDto } from './../model'; describe('RolesService', () => { - const version = new Version('1'); + const version = new VersionTag('1'); beforeEach(() => { TestBed.configureTestingModule({ @@ -32,7 +33,6 @@ describe('RolesService', () => { it('should make get request to get all permissions', inject([RolesService, HttpTestingController], (roleService: RolesService, httpMock: HttpTestingController) => { let permissions: ReadonlyArray; - roleService.getPermissions('my-app').subscribe(result => { permissions = result; }); @@ -49,8 +49,7 @@ describe('RolesService', () => { it('should make get request to get roles', inject([RolesService, HttpTestingController], (roleService: RolesService, httpMock: HttpTestingController) => { - let roles: RolesDto; - + let roles: Versioned; roleService.getRoles('my-app').subscribe(result => { roles = result; }); @@ -66,15 +65,14 @@ describe('RolesService', () => { }, }); - expect(roles!).toEqual({ payload: createRoles(2, 4), version: new Version('2') }); + expect(roles!).toEqual({ payload: createRoles(2, 4), version: new VersionTag('2') }); })); it('should make post request to add role', inject([RolesService, HttpTestingController], (roleService: RolesService, httpMock: HttpTestingController) => { - const dto = { name: 'Role3' }; - - let roles: RolesDto; + const dto = new AddRoleDto({ name: 'Role3' }); + let roles: Versioned; roleService.postRole('my-app', dto, version).subscribe(result => { roles = result; }); @@ -90,12 +88,12 @@ describe('RolesService', () => { }, }); - expect(roles!).toEqual({ payload: createRoles(2, 4), version: new Version('2') }); + expect(roles!).toEqual({ payload: createRoles(2, 4), version: new VersionTag('2') }); })); it('should make put request to update role', inject([RolesService, HttpTestingController], (roleService: RolesService, httpMock: HttpTestingController) => { - const dto = { permissions: ['P4', 'P5'], properties: createProperties(1) }; + const dto = new UpdateRoleDto({ permissions: [], properties: createProperties(1) }); const resource: Resource = { _links: { @@ -103,8 +101,7 @@ describe('RolesService', () => { }, }; - let roles: RolesDto; - + let roles: Versioned; roleService.putRole('my-app', resource, dto, version).subscribe(result => { roles = result; }); @@ -120,7 +117,7 @@ describe('RolesService', () => { }, }); - expect(roles!).toEqual({ payload: createRoles(2, 4), version: new Version('2') }); + expect(roles!).toEqual({ payload: createRoles(2, 4), version: new VersionTag('2') }); })); it('should make delete request to remove role', @@ -131,8 +128,7 @@ describe('RolesService', () => { }, }; - let roles: RolesDto; - + let roles: Versioned; roleService.deleteRole('my-app', resource, version).subscribe(result => { roles = result; }); @@ -148,7 +144,7 @@ describe('RolesService', () => { }, }); - expect(roles!).toEqual({ payload: createRoles(2, 4), version: new Version('2') }); + expect(roles!).toEqual({ payload: createRoles(2, 4), version: new VersionTag('2') }); })); function rolesResponse(...ids: number[]) { @@ -171,36 +167,37 @@ describe('RolesService', () => { } }); -export function createRoles(...ids: ReadonlyArray): RolesPayload { - return { +export function createRoles(...ids: ReadonlyArray) { + return new RolesDto({ items: ids.map(createRole), - canCreate: true, - }; + _links: { + create: new ResourceLinkDto({ method: 'POST', href: '/roles' }), + }, + }); } export function createRole(id: number) { - const links: ResourceLinks = { - update: { method: 'PUT', href: `/roles/id${id}` }, - }; - - return new RoleDto(links, `name${id}`, id * 2, id * 3, - createPermissions(id), - createProperties(id), - id % 2 === 0); + return new RoleDto({ + name: `name${id}`, + numClients: id * 2, + numContributors: id * 3, + permissions: createPermissions(id), + properties: createProperties(id), + isDefaultRole: id % 2 === 0, + _links: { + update: new ResourceLinkDto({ method: 'PUT', href: `/roles/id${id}` }), + }, + }); } function createPermissions(id: number) { const result: string[] = []; - result.push(`permission${id}`); - return result; } function createProperties(id: number) { const result = {} as Record; - result[`property${id}`] = true; - return result; } diff --git a/frontend/src/app/shared/services/roles.service.ts b/frontend/src/app/shared/services/roles.service.ts index b7d23d49e..2162082e7 100644 --- a/frontend/src/app/shared/services/roles.service.ts +++ b/frontend/src/app/shared/services/roles.service.ts @@ -8,51 +8,8 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { ApiUrlConfig, hasAnyLink, HTTP, mapVersioned, pretifyError, Resource, ResourceLinks, Version, Versioned } from '@app/framework'; - -export class RoleDto { - public readonly _links: ResourceLinks; - - public readonly canDelete: boolean; - public readonly canUpdate: boolean; - - constructor(links: ResourceLinks, - public readonly name: string, - public readonly numClients: number, - public readonly numContributors: number, - public readonly permissions: ReadonlyArray, - public readonly properties: {}, - public readonly isDefaultRole: boolean, - ) { - this._links = links; - - this.canDelete = hasAnyLink(links, 'delete'); - this.canUpdate = hasAnyLink(links, 'update'); - } -} - -export type RolesDto = Versioned; - -export type RolesPayload = Readonly<{ - // The list of roles. - items: ReadonlyArray; - - // True, if the user has permissions to create a new role. - canCreate?: boolean; -}>; - -export type CreateRoleDto = Readonly<{ - // The name of the role, cannot be changed later. - name: string; -}>; - -export type UpdateRoleDto = Readonly<{ - // The permissions in dot notation. - permissions: ReadonlyArray; - - // The UI properties. - properties: {}; -}>; +import { ApiUrlConfig, HTTP, mapVersioned, pretifyError, Resource, Versioned, VersionOrTag } from '@app/framework'; +import { AddRoleDto, RolesDto, UpdateRoleDto } from '../model'; @Injectable({ providedIn: 'root', @@ -64,46 +21,46 @@ export class RolesService { ) { } - public getRoles(appName: string): Observable { + public getRoles(appName: string): Observable> { const url = this.apiUrl.buildUrl(`api/apps/${appName}/roles`); return HTTP.getVersioned(this.http, url).pipe( mapVersioned(({ body }) => { - return parseRoles(body); + return RolesDto.fromJSON(body); }), pretifyError('i18n:roles.loadFailed')); } - public postRole(appName: string, dto: CreateRoleDto, version: Version): Observable { + public postRole(appName: string, dto: AddRoleDto, version: VersionOrTag): Observable> { const url = this.apiUrl.buildUrl(`api/apps/${appName}/roles`); - return HTTP.postVersioned(this.http, url, dto, version).pipe( + return HTTP.postVersioned(this.http, url, dto.toJSON(), version).pipe( mapVersioned(({ body }) => { - return parseRoles(body); + return RolesDto.fromJSON(body); }), pretifyError('i18n:roles.addFailed')); } - public putRole(appName: string, resource: Resource, dto: UpdateRoleDto, version: Version): Observable { + public putRole(appName: string, resource: Resource, dto: UpdateRoleDto, version: VersionOrTag): Observable> { const link = resource._links['update']; const url = this.apiUrl.buildUrl(link.href); - return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe( + return HTTP.requestVersioned(this.http, link.method, url, version, dto.toJSON()).pipe( mapVersioned(({ body }) => { - return parseRoles(body); + return RolesDto.fromJSON(body); }), pretifyError('i18n:roles.updateFailed')); } - public deleteRole(appName: string, resource: Resource, version: Version): Observable { + public deleteRole(appName: string, resource: Resource, version: VersionOrTag): Observable> { const link = resource._links['delete']; const url = this.apiUrl.buildUrl(link.href); return HTTP.requestVersioned(this.http, link.method, url, version).pipe( mapVersioned(({ body }) => { - return parseRoles(body); + return RolesDto.fromJSON(body); }), pretifyError('i18n:roles.revokeFailed')); } @@ -114,23 +71,4 @@ export class RolesService { return this.http.get(url).pipe( pretifyError('i18n:roles.loadPermissionsFailed')); } -} - -function parseRoles(response: { items: any } & Resource): RolesPayload { - const { items: list, _links } = response; - const items = list.map(parseRole); - - const canCreate = hasAnyLink(_links, 'create'); - - return { items, canCreate }; -} - -function parseRole(response: any) { - return new RoleDto(response._links, - response.name, - response.numClients, - response.numContributors, - response.permissions, - response.properties, - response.isDefaultRole); } \ No newline at end of file diff --git a/frontend/src/app/shared/services/rules.service.spec.ts b/frontend/src/app/shared/services/rules.service.spec.ts index d815ce0cd..0d1fbad73 100644 --- a/frontend/src/app/shared/services/rules.service.spec.ts +++ b/frontend/src/app/shared/services/rules.service.spec.ts @@ -8,11 +8,10 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { ApiUrlConfig, DateTime, Resource, ResourceLinks, RuleDto, RuleElementDto, RuleElementPropertyDto, RuleEventDto, RuleEventsDto, RulesDto, RulesService, ScriptCompletions, Version } from '@app/shared/internal'; -import { SimulatedRuleEventDto, SimulatedRuleEventsDto } from './rules.service'; +import { ApiUrlConfig, ContentChangedRuleTriggerDto, DateTime, DynamicCreateRuleDto, DynamicRuleDto, DynamicRulesDto, DynamicUpdateRuleDto, ManualRuleTriggerDto, Resource, ResourceLinkDto, RuleElementDto, RuleElementPropertyDto, RuleEventDto, RuleEventsDto, RulesService, ScriptCompletions, SimulatedRuleEventDto, SimulatedRuleEventsDto, VersionTag } from '@app/shared/internal'; describe('RulesService', () => { - const version = new Version('1'); + const version = new VersionTag('1'); beforeEach(() => { TestBed.configureTestingModule({ @@ -33,7 +32,6 @@ describe('RulesService', () => { it('should make get request to get actions', inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { let actions: { [ name: string ]: RuleElementDto }; - rulesService.getActions().subscribe(result => { actions = result; }); @@ -51,25 +49,28 @@ describe('RulesService', () => { iconColor: '#222', iconImage: '', readMore: 'link2', - properties: [{ - name: 'property1', - editor: 'Editor1', - display: 'Display1', - description: 'Description1', - isRequired: true, - isFormattable: false, - }, { - name: 'property2', - editor: 'Editor2', - display: 'Display2', - description: 'Description2', - isRequired: false, - isFormattable: true, - options: [ - 'Yes', - 'No', - ], - }], + properties: [ + { + name: 'property1', + editor: 'Editor1', + display: 'Display1', + description: 'Description1', + isRequired: true, + isFormattable: false, + }, + { + name: 'property2', + editor: 'Editor2', + display: 'Display2', + description: 'Description2', + isRequired: false, + isFormattable: true, + options: [ + 'Yes', + 'No', + ], + }, + ], }, action1: { title: 'title1', @@ -82,23 +83,52 @@ describe('RulesService', () => { }, }); - const action1 = new RuleElementDto('title1', 'display1', 'description1', '#111', '', null, 'link1', []); - - const action2 = new RuleElementDto('title2', 'display2', 'description2', '#222', '', null, 'link2', [ - new RuleElementPropertyDto('property1', 'Editor1', 'Display1', 'Description1', false, true), - new RuleElementPropertyDto('property2', 'Editor2', 'Display2', 'Description2', true, false, ['Yes', 'No']), - ]); - expect(actions!).toEqual({ - action1, - action2, + action2: new RuleElementDto({ + title: 'title2', + display: 'display2', + description: 'description2', + iconColor: '#222', + iconImage: '', + readMore: 'link2', + properties: [ + new RuleElementPropertyDto({ + name: 'property1', + editor: 'Editor1' as any, + display: 'Display1', + description: 'Description1', + isRequired: true, + isFormattable: false, + }), + new RuleElementPropertyDto({ + name: 'property2', + editor: 'Editor2' as any, + display: 'Display2', + description: 'Description2', + isRequired: false, + isFormattable: true, + options: [ + 'Yes', + 'No', + ], + }), + ], + }), + action1: new RuleElementDto({ + title: 'title1', + display: 'display1', + description: 'description1', + iconColor: '#111', + iconImage: '', + readMore: 'link1', + properties: [], + }), }); })); it('should make get request to get app rules', inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { - let rules: RulesDto; - + let rules: DynamicRulesDto; rulesService.getRules('my-app').subscribe(result => { rules = result; }); @@ -114,37 +144,31 @@ describe('RulesService', () => { ruleResponse(13), ], runningRuleId: '12', + _links: {}, }); - expect(rules!).toEqual({ + expect(rules!).toEqual(new DynamicRulesDto({ items: [ createRule(12), createRule(13), ], runningRuleId: '12', - canCancelRun: false, - canCreate: false, - canReadEvents: false, - }); + _links: {}, + })); })); it('should make post request to create rule', inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { - const dto = { - trigger: { - param1: 1, - param2: 2, - triggerType: 'ContentChanged', - }, + const dto = new DynamicCreateRuleDto({ + trigger: new ManualRuleTriggerDto(), action: { param3: 3, param4: 4, actionType: 'Webhook', }, - }; - - let rule: RuleDto; + }); + let rule: DynamicRuleDto; rulesService.postRule('my-app', dto).subscribe(result => { rule = result; }); @@ -161,14 +185,14 @@ describe('RulesService', () => { it('should make put request to update rule', inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { - const dto: any = { - trigger: { - param1: 1, - }, + const dto = new DynamicUpdateRuleDto({ + trigger: new ManualRuleTriggerDto(), action: { - param3: 2, + param3: 3, + param4: 4, + actionType: 'Webhook', }, - }; + }); const resource: Resource = { _links: { @@ -176,8 +200,7 @@ describe('RulesService', () => { }, }; - let rule: RuleDto; - + let rule: DynamicRuleDto; rulesService.putRule('my-app', resource, dto, version).subscribe(result => { rule = result; }); @@ -279,7 +302,6 @@ describe('RulesService', () => { it('should make get request to get rule events', inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { let rules: RuleEventsDto; - rulesService.getEvents('my-app', 10, 20, '12').subscribe(result => { rules = result; }); @@ -299,23 +321,21 @@ describe('RulesService', () => { }, }); - expect(rules!).toEqual({ + expect(rules!).toEqual(new RuleEventsDto({ + total: 20, items: [ createRuleEvent(1), createRuleEvent(2), ], _links: { - cancel: { method: 'DELETE', href: '/rules/events' }, + cancel: new ResourceLinkDto({ method: 'DELETE', href: '/rules/events' }), }, - total: 20, - canCancelAll: false, - }); + })); })); it('should make get request to get simulated rule events', inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { let rules: SimulatedRuleEventsDto; - rulesService.getSimulatedEvents('my-app', '12').subscribe(result => { rules = result; }); @@ -330,21 +350,22 @@ describe('RulesService', () => { simulatedRuleEventResponse(1), simulatedRuleEventResponse(2), ], + _links: {}, }); - expect(rules!).toEqual({ + expect(rules!).toEqual(new SimulatedRuleEventsDto({ + total: 20, items: [ createSimulatedRuleEvent(1), createSimulatedRuleEvent(2), ], - total: 20, - }); + _links: {}, + })); })); it('should make post request to get simulated rule events with action and trigger', inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { let rules: SimulatedRuleEventsDto; - rulesService.postSimulatedEvents('my-app', {}, {}).subscribe(result => { rules = result; }); @@ -359,15 +380,17 @@ describe('RulesService', () => { simulatedRuleEventResponse(1), simulatedRuleEventResponse(2), ], + _links: {}, }); - expect(rules!).toEqual({ + expect(rules!).toEqual(new SimulatedRuleEventsDto({ + total: 20, items: [ createSimulatedRuleEvent(1), createSimulatedRuleEvent(2), ], - total: 20, - }); + _links: {}, + })); })); it('should make put request to enqueue rule event', @@ -409,7 +432,6 @@ describe('RulesService', () => { it('should make get request to get completions', inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { let completions: ScriptCompletions; - rulesService.getCompletions('my-app', 'TriggerType').subscribe(result => { completions = result; }); @@ -431,16 +453,14 @@ describe('RulesService', () => { id: `id${id}`, created: buildDate(id, 10), createdBy: `creator${id}`, + isEnabled: id % 2 === 0, lastModified: buildDate(id, 20), lastModifiedBy: `modifier${id}`, name: `rule-name${key}`, - numSucceeded: id * 3, numFailed: id * 4, - isEnabled: id % 2 === 0, + numSucceeded: id * 3, trigger: { - param1: 1, - param2: 2, - triggerType: `rule-trigger${key}`, + triggerType: 'ContentChanged', }, action: { param3: 3, @@ -486,74 +506,69 @@ describe('RulesService', () => { actionData: `action-data${key}`, skipReasons: [`reason${key}`], uniqueId: `unique-id${key}`, - _links: {}, }; } }); export function createRule(id: number, suffix = '') { - const links: ResourceLinks = { - update: { method: 'PUT', href: `/rules/${id}` }, - }; - const key = `${id}${suffix}`; - return new RuleDto(links, - `id${id}`, - DateTime.parseISO(buildDate(id, 10)), `creator${id}`, - DateTime.parseISO(buildDate(id, 20)), `modifier${id}`, - new Version(key), - id % 2 === 0, - { - param1: 1, - param2: 2, - triggerType: `rule-trigger${key}`, - }, - `rule-trigger${key}` as any, - { + return new DynamicRuleDto({ + id: `id${id}`, + created: DateTime.parseISO(buildDate(id, 10)), + createdBy: `creator${id}`, + isEnabled: id % 2 === 0, + lastModified: DateTime.parseISO(buildDate(id, 20)), + lastModifiedBy: `modifier${id}`, + name: `rule-name${key}`, + numFailed: id * 4, + numSucceeded: id * 3, + trigger: new ContentChangedRuleTriggerDto(), + action: { param3: 3, param4: 4, actionType: `rule-action${key}`, + } as any, + version: id, + _links: { + update: new ResourceLinkDto({ method: 'PUT', href: `/rules/${id}` }), }, - `rule-action${key}`, - `rule-name${key}`, - id * 3, - id * 4); + }); } export function createRuleEvent(id: number, suffix = '') { - const links: ResourceLinks = { - update: { method: 'PUT', href: `/rules/events/${id}` }, - }; - const key = `${id}${suffix}`; - return new RuleEventDto(links, `id${id}`, - DateTime.parseISO(buildDate(id, 10)), - DateTime.parseISO(buildDate(id, 20)), - `event-name${key}`, - `event-url${key}`, - `event-dump${key}`, - `Failed${key}`, - `Failed${key}`, - id); + return new RuleEventDto({ + id: `id${id}`, + created: DateTime.parseISO(buildDate(id, 10)), + description: `event-url${key}`, + eventName: `event-name${key}`, + jobResult: `Failed${key}` as any, + lastDump: `event-dump${key}`, + nextAttempt: DateTime.parseISO(buildDate(id, 20)), + numCalls: id, + result: `Failed${key}` as any, + _links: { + update: new ResourceLinkDto({ method: 'PUT', href: `/rules/events/${id}` }), + }, + }); } export function createSimulatedRuleEvent(id: number, suffix = '') { const key = `${id}${suffix}`; - return new SimulatedRuleEventDto({}, - `id${key}`, - `name${key}`, - { value: 'simple' }, - { value: 'enriched' }, - `action-name${key}`, - `action-data${key}`, - `error${key}`, - [ - `reason${key}`, - ], - `unique-id${key}`); + return new SimulatedRuleEventDto({ + eventId: `id${key}`, + eventName: `name${key}`, + event: { value: 'simple' }, + enrichedEvent: { value: 'enriched' }, + error: `error${key}`, + actionName: `action-name${key}`, + actionData: `action-data${key}`, + skipReasons: [`reason${key}` as any], + uniqueId: `unique-id${key}`, + }); } function buildDate(id: number, add = 0) { diff --git a/frontend/src/app/shared/services/rules.service.ts b/frontend/src/app/shared/services/rules.service.ts index ecc6d2d13..81d97c18c 100644 --- a/frontend/src/app/shared/services/rules.service.ts +++ b/frontend/src/app/shared/services/rules.service.ts @@ -9,7 +9,8 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { ApiUrlConfig, DateTime, hasAnyLink, HTTP, Model, pretifyError, Resource, ResourceLinks, ScriptCompletions, StringHelper, Version } from '@app/framework'; +import { ApiUrlConfig, HTTP, pretifyError, Resource, ScriptCompletions, StringHelper, VersionOrTag } from '@app/framework'; +import { DynamicCreateRuleDto, DynamicRuleDto, DynamicRulesDto, DynamicUpdateRuleDto, RuleElementDto, RuleEventsDto, SimulatedRuleEventsDto } from './../model'; export type RuleElementMetadataDto = Readonly<{ description: string; @@ -21,15 +22,8 @@ export type RuleElementMetadataDto = Readonly<{ readMore?: string; }>; -export type TriggerType = - 'AssetChanged' | - 'Comment' | - 'ContentChanged' | - 'Manual' | - 'SchemaChanged' | - 'Usage'; -export type TriggersDto = Record; +export type TriggersDto = Record; export const ALL_TRIGGERS: TriggersDto = { AssetChanged: { @@ -76,188 +70,7 @@ export const ALL_TRIGGERS: TriggersDto = { }, }; -export class RuleElementDto { - constructor( - public readonly title: string, - public readonly display: string, - public readonly description: string, - public readonly iconColor: string, - public readonly iconImage: string, - public readonly iconCode: string | null, - public readonly readMore: string, - public readonly properties: ReadonlyArray, - ) { - } -} - -export class RuleElementPropertyDto { - constructor( - public readonly name: string, - public readonly editor: string, - public readonly display: string, - public readonly description: string, - public readonly isFormattable: boolean, - public readonly isRequired: boolean, - public readonly options?: ReadonlyArray, - ) { - } -} - -export class RuleDto { - public readonly _links: ResourceLinks; - - public readonly canDelete: boolean; - public readonly canDisable: boolean; - public readonly canEnable: boolean; - public readonly canReadLogs: boolean; - public readonly canRun: boolean; - public readonly canRunFromSnapshots: boolean; - public readonly canTrigger: boolean; - public readonly canUpdate: boolean; - - constructor(links: ResourceLinks, - public readonly id: string, - public readonly created: DateTime, - public readonly createdBy: string, - public readonly lastModified: DateTime, - public readonly lastModifiedBy: string, - public readonly version: Version, - public readonly isEnabled: boolean, - public readonly trigger: any, - public readonly triggerType: TriggerType, - public readonly action: any, - public readonly actionType: string, - public readonly name: string, - public readonly numSucceeded: number, - public readonly numFailed: number, - ) { - this._links = links; - - this.canDelete = hasAnyLink(links, 'delete'); - this.canDisable = hasAnyLink(links, 'disable'); - this.canEnable = hasAnyLink(links, 'enable'); - this.canReadLogs = hasAnyLink(links, 'logs'); - this.canRun = hasAnyLink(links, 'run'); - this.canRunFromSnapshots = hasAnyLink(links, 'run/snapshots'); - this.canTrigger = hasAnyLink(links, 'trigger'); - this.canUpdate = hasAnyLink(links, 'update'); - } -} - -export class RuleEventDto extends Model { - public readonly _links: ResourceLinks; - - public readonly canDelete: boolean; - public readonly canUpdate: boolean; - - constructor(links: ResourceLinks, - public readonly id: string, - public readonly created: DateTime, - public readonly nextAttempt: DateTime | null, - public readonly eventName: string, - public readonly description: string, - public readonly lastDump: string, - public readonly result: string, - public readonly jobResult: string, - public readonly numCalls: number, - ) { - super(); - - this._links = links; - - this.canDelete = hasAnyLink(links, 'cancel'); - this.canUpdate = hasAnyLink(links, 'update'); - } -} - -export class SimulatedRuleEventDto { - public readonly _links: ResourceLinks; - - constructor(links: ResourceLinks, - public readonly eventId: string, - public readonly eventName: string, - public readonly event: any, - public readonly enrichedEvent: any | undefined, - public readonly actionName: string | undefined, - public readonly actionData: string | undefined, - public readonly error: string | undefined, - public readonly skipReasons: ReadonlyArray, - public readonly uniqueId: string, - ) { - this._links = links; - } -} - -export type RulesDto = Readonly<{ - // The list of rules. - items: ReadonlyArray; - - // The id of the rule that is currently running. - runningRuleId?: string; - - // True, if the user has permission to create a rule. - canCreate?: boolean; - - // True, if the user has permission to read events. - canReadEvents?: boolean; - - // True, if the user has permission to cancel an event. - canCancelRun?: boolean; -}>; - -export type RuleEventsDto = Readonly<{ - // The list of rule events. - items: ReadonlyArray; - - // The total number of rule events. - total: number; - - // True, if the user has permissions to cancel all rule events. - canCancelAll?: boolean; -}> & Resource; - -export type SimulatedRuleEventsDto = Readonly<{ - // The list of simulated rule events. - items: ReadonlyArray; - - // The total number of simulated rule events. - total: number; -}>; - -export type ActionsDto = Readonly<{ - // The rule elements by name. - [name: string]: RuleElementDto; -}>; - -export type UpsertRuleDto = Readonly<{ - // The optional trigger to update. - trigger?: RuleTrigger; - - // The optional action to update. - action?: RuleAction; - - // The optional rule name. - name?: string; - - // True, if the rule is enabled. - isEnabled?: boolean; -}>; - -export type RuleAction = Readonly<{ - // The type of the action. - actionType: string; - - // The additional properties. - [key: string]: any; - }>; - -export type RuleTrigger = Readonly<{ - // The type of the trigger. - triggerType: string; - - // The additional properties. - [key: string]: any; -}>; +export type ActionsDto = Readonly<{ [name: string]: RuleElementDto }>; @Injectable({ providedIn: 'root', @@ -269,49 +82,54 @@ export class RulesService { ) { } - public getActions(): Observable<{ [name: string]: RuleElementDto }> { + public getActions(): Observable { const url = this.apiUrl.buildUrl('api/rules/actions'); - return this.http.get(url).pipe( + return this.http.get>(url).pipe( map(body => { - return parseActions(body); + const result: { [name: string]: RuleElementDto } = {}; + for (const [key, value] of Object.entries(body)) { + result[key] = RuleElementDto.fromJSON(value); + } + + return result; }), pretifyError('i18n:rules.loadFailed')); } - public getRules(appName: string): Observable { + public getRules(appName: string): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules`); return this.http.get(url).pipe( map(body => { - return parseRules(body); + return DynamicRulesDto.fromJSON(body); }), pretifyError('i18n:rules.loadFailed')); } - public postRule(appName: string, dto: UpsertRuleDto): Observable { + public postRule(appName: string, dto: DynamicCreateRuleDto): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules`); - return HTTP.postVersioned(this.http, url, dto).pipe( + return HTTP.postVersioned(this.http, url, dto.toJSON()).pipe( map(({ payload }) => { - return parseRule(payload.body); + return DynamicRuleDto.fromJSON(payload.body); }), pretifyError('i18n:rules.createFailed')); } - public putRule(appName: string, resource: Resource, dto: Partial, version: Version): Observable { + public putRule(appName: string, resource: Resource, dto: DynamicUpdateRuleDto, version: VersionOrTag): Observable { const link = resource._links['update']; const url = this.apiUrl.buildUrl(link.href); - return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe( + return HTTP.requestVersioned(this.http, link.method, url, version, dto.toJSON()).pipe( map(({ payload }) => { - return parseRule(payload.body); + return DynamicRuleDto.fromJSON(payload.body); }), pretifyError('i18n:rules.updateFailed')); } - public deleteRule(appName: string, resource: Resource, version: Version): Observable { + public deleteRule(appName: string, resource: Resource, version: VersionOrTag): Observable { const link = resource._links['delete']; const url = this.apiUrl.buildUrl(link.href); @@ -359,7 +177,7 @@ export class RulesService { return this.http.get(url).pipe( map(body => { - return parseEvents(body); + return RuleEventsDto.fromJSON(body); }), pretifyError('i18n:rules.ruleEvents.loadFailed')); } @@ -369,7 +187,7 @@ export class RulesService { return this.http.get(url).pipe( map(body => { - return parseSimulatedEvents(body); + return SimulatedRuleEventsDto.fromJSON(body); }), pretifyError('i18n:rules.ruleEvents.loadFailed')); } @@ -379,7 +197,7 @@ export class RulesService { return this.http.post(url, { trigger, action }).pipe( map(body => { - return parseSimulatedEvents(body); + return SimulatedRuleEventsDto.fromJSON(body); }), pretifyError('i18n:rules.ruleEvents.loadFailed')); } @@ -407,103 +225,4 @@ export class RulesService { return this.http.get(url); } -} - -function parseSimulatedEvents(response: { items: any[]; total: number } & Resource): SimulatedRuleEventsDto { - const { items: list, total } = response; - const items = list.map(parseSimulatedRuleEvent); - - return { items, total }; -} - -function parseEvents(response: { items: any[]; total: number } & Resource): RuleEventsDto { - const { items: list, total, _links } = response; - const items = list.map(parseRuleEvent); - - const canCancelAll = hasAnyLink(_links, 'create'); - - return { items, total, canCancelAll, _links }; -} - -function parseRules(response: { items: any[]; runningRuleId?: string } & Resource): RulesDto { - const { items: list, runningRuleId, _links } = response; - const items = list.map(parseRule); - - const canCreate = hasAnyLink(_links, 'create'); - const canReadEvents = hasAnyLink(_links, 'events'); - const canCancelRun = hasAnyLink(_links, 'run/cancel'); - - return { items, runningRuleId, canCreate, canCancelRun, canReadEvents }; -} - -function parseActions(response: any) { - const actions: { [name: string]: RuleElementDto } = {}; - - for (const key of Object.keys(response).sort()) { - const value = response[key]; - - const properties = value.properties.map((property: any) => - new RuleElementPropertyDto( - property.name, - property.editor, - property.display, - property.description, - property.isFormattable, - property.isRequired, - property.options, - )); - - actions[key] = new RuleElementDto( - value.title, - value.display, - value.description, - value.iconColor, - value.iconImage, null, - value.readMore, - properties); - } - - return actions; -} - -function parseRule(response: any) { - return new RuleDto(response._links, - response.id, - DateTime.parseISO(response.created), response.createdBy, - DateTime.parseISO(response.lastModified), response.lastModifiedBy, - new Version(response.version.toString()), - response.isEnabled, - response.trigger, - response.trigger.triggerType, - response.action, - response.action.actionType, - response.name, - response.numSucceeded, - response.numFailed); -} - -function parseRuleEvent(response: any) { - return new RuleEventDto(response._links, - response.id, - DateTime.parseISO(response.created), - response.nextAttempt ? DateTime.parseISO(response.nextAttempt) : null, - response.eventName, - response.description, - response.lastDump, - response.result, - response.jobResult, - response.numCalls); -} - -function parseSimulatedRuleEvent(response: any) { - return new SimulatedRuleEventDto(response._links, - response.eventId, - response.eventName, - response.event, - response.enrichedEvent, - response.actionName, - response.actionData, - response.error, - response.skipReasons, - response.uniqueId); -} +} \ No newline at end of file diff --git a/frontend/src/app/shared/services/schemas.service.spec.ts b/frontend/src/app/shared/services/schemas.service.spec.ts index b955a1160..74e7932ca 100644 --- a/frontend/src/app/shared/services/schemas.service.spec.ts +++ b/frontend/src/app/shared/services/schemas.service.spec.ts @@ -8,10 +8,10 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { ApiUrlConfig, createProperties, DateTime, FieldRule, NestedFieldDto, Resource, ResourceLinks, RootFieldDto, SchemaDto, SchemaPropertiesDto, SchemasDto, SchemasService, ScriptCompletions, Version } from '@app/shared/internal'; +import { AddFieldDto, ApiUrlConfig, ArrayFieldPropertiesDto, AssetsFieldPropertiesDto, BooleanFieldPropertiesDto, ChangeCategoryDto, ComponentFieldPropertiesDto, ComponentsFieldPropertiesDto, ConfigureFieldRulesDto, ConfigureUIFieldsDto, createProperties, CreateSchemaDto, DateTime, DateTimeFieldPropertiesDto, FieldDto, FieldRuleDto, GeolocationFieldPropertiesDto, JsonFieldPropertiesDto, NestedFieldDto, NumberFieldPropertiesDto, ReferencesFieldPropertiesDto, Resource, ResourceLinkDto, SchemaDto, SchemaPropertiesDto, SchemaScriptsDto, SchemasDto, SchemasService, ScriptCompletions, StringFieldPropertiesDto, SynchronizeSchemaDto, TagsFieldPropertiesDto, UpdateFieldDto, UpdateSchemaDto, VersionTag } from '@app/shared/internal'; describe('SchemasService', () => { - const version = new Version('1'); + const version = new VersionTag('1'); beforeEach(() => { TestBed.configureTestingModule({ @@ -38,7 +38,6 @@ describe('SchemasService', () => { it('should make get request to get schemas', inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => { let schemas: SchemasDto; - schemasService.getSchemas('my-app').subscribe(result => { schemas = result; }); @@ -58,19 +57,20 @@ describe('SchemasService', () => { }, }); - expect(schemas!).toEqual({ + expect(schemas!).toEqual(new SchemasDto({ items: [ createSchema(12), createSchema(13), ], - canCreate: true, - }); + _links: { + create: new ResourceLinkDto({ method: 'POST', href: '/schemas' }), + }, + })); })); it('should make get request to get schema', inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => { let schema: SchemaDto; - schemasService.getSchema('my-app', 'my-schema').subscribe(result => { schema = result; }); @@ -87,10 +87,9 @@ describe('SchemasService', () => { it('should make post request to create schema', inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => { - const dto = { name: 'name' }; + const dto = new CreateSchemaDto({ name: 'name' }); let schema: SchemaDto; - schemasService.postSchema('my-app', dto).subscribe(result => { schema = result; }); @@ -107,7 +106,7 @@ describe('SchemasService', () => { it('should make put request to update schema', inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => { - const dto = { label: 'label1' }; + const dto = new UpdateSchemaDto({ label: 'label1' }); const resource: Resource = { _links: { @@ -116,7 +115,6 @@ describe('SchemasService', () => { }; let schema: SchemaDto; - schemasService.putSchema('my-app', resource, dto, version).subscribe(result => { schema = result; }); @@ -142,7 +140,6 @@ describe('SchemasService', () => { }; let schema: SchemaDto; - schemasService.putScripts('my-app', resource, dto, version).subscribe(result => { schema = result; }); @@ -159,7 +156,7 @@ describe('SchemasService', () => { it('should make put request to update field rules', inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => { - const dto: FieldRule[] = [{ field: 'field1', action: 'Disable', condition: 'a === b' }]; + const dto = new ConfigureFieldRulesDto({}); const resource: Resource = { _links: { @@ -168,7 +165,6 @@ describe('SchemasService', () => { }; let schema: SchemaDto; - schemasService.putFieldRules('my-app', resource, dto, version).subscribe(result => { schema = result; }); @@ -185,7 +181,7 @@ describe('SchemasService', () => { it('should make put request to synchronize schema', inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => { - const dto = {}; + const dto = new SynchronizeSchemaDto({}); const resource: Resource = { _links: { @@ -194,7 +190,6 @@ describe('SchemasService', () => { }; let schema: SchemaDto; - schemasService.putSchemaSync('my-app', resource, dto, version).subscribe(result => { schema = result; }); @@ -211,7 +206,7 @@ describe('SchemasService', () => { it('should make put request to update category', inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => { - const dto = {}; + const dto = new ChangeCategoryDto({}); const resource: Resource = { _links: { @@ -220,7 +215,6 @@ describe('SchemasService', () => { }; let schema: SchemaDto; - schemasService.putCategory('my-app', resource, dto, version).subscribe(result => { schema = result; }); @@ -246,7 +240,6 @@ describe('SchemasService', () => { }; let schema: SchemaDto; - schemasService.putPreviewUrls('my-app', resource, dto, version).subscribe(result => { schema = result; }); @@ -263,7 +256,11 @@ describe('SchemasService', () => { it('should make post request to add field', inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => { - const dto = { name: 'name', partitioning: 'invariant', properties: createProperties('Number') }; + const dto = new AddFieldDto({ + name: 'name', + partitioning: 'invariant', + properties: createProperties('Number'), + }); const resource: Resource = { _links: { @@ -272,7 +269,6 @@ describe('SchemasService', () => { }; let schema: SchemaDto; - schemasService.postField('my-app', resource, dto, version).subscribe(result => { schema = result; }); @@ -296,7 +292,6 @@ describe('SchemasService', () => { }; let schema: SchemaDto; - schemasService.publishSchema('my-app', resource, version).subscribe(result => { schema = result; }); @@ -320,7 +315,6 @@ describe('SchemasService', () => { }; let schema: SchemaDto; - schemasService.unpublishSchema('my-app', resource, version).subscribe(result => { schema = result; }); @@ -337,7 +331,7 @@ describe('SchemasService', () => { it('should make put request to update field', inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => { - const dto = { properties: createProperties('Number') }; + const dto = new UpdateFieldDto({ properties: createProperties('Number') }); const resource: Resource = { _links: { @@ -346,7 +340,6 @@ describe('SchemasService', () => { }; let schema: SchemaDto; - schemasService.putField('my-app', resource, dto, version).subscribe(result => { schema = result; }); @@ -363,7 +356,7 @@ describe('SchemasService', () => { it('should make put request to update ui fields', inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => { - const dto = { fieldsInReferences: ['field1'] }; + const dto = new ConfigureUIFieldsDto({ fieldsInReferences: ['field1'] }); const resource: Resource = { _links: { @@ -372,7 +365,6 @@ describe('SchemasService', () => { }; let schema: SchemaDto; - schemasService.putUIFields('my-app', resource, dto, version).subscribe(result => { schema = result; }); @@ -422,7 +414,6 @@ describe('SchemasService', () => { }; let schema: SchemaDto; - schemasService.lockField('my-app', resource, version).subscribe(result => { schema = result; }); @@ -446,7 +437,6 @@ describe('SchemasService', () => { }; let schema: SchemaDto; - schemasService.enableField('my-app', resource, version).subscribe(result => { schema = result; }); @@ -494,7 +484,6 @@ describe('SchemasService', () => { }; let schema: SchemaDto; - schemasService.showField('my-app', resource, version).subscribe(result => { schema = result; }); @@ -518,7 +507,6 @@ describe('SchemasService', () => { }; let schema: SchemaDto; - schemasService.hideField('my-app', resource, version).subscribe(result => { schema = result; }); @@ -542,7 +530,6 @@ describe('SchemasService', () => { }; let schema: SchemaDto; - schemasService.deleteField('my-app', resource, version).subscribe(result => { schema = result; }); @@ -578,7 +565,6 @@ describe('SchemasService', () => { it('should make get request to get content scripts completions', inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => { let completions: ScriptCompletions; - schemasService.getContentScriptsCompletion('my-app', 'my-schema').subscribe(result => { completions = result; }); @@ -596,7 +582,6 @@ describe('SchemasService', () => { it('should make get request to get content trigger completions', inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => { let completions: ScriptCompletions; - schemasService.getContentTriggerCompletion('my-app', 'my-schema').subscribe(result => { completions = result; }); @@ -614,7 +599,6 @@ describe('SchemasService', () => { it('should make get request to get field rules completions', inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => { let completions: ScriptCompletions; - schemasService.getFieldRulesCompletion('my-app', 'my-schema').subscribe(result => { completions = result; }); @@ -632,7 +616,6 @@ describe('SchemasService', () => { it('should make get request to get preview urls completions', inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => { let completions: ScriptCompletions; - schemasService.getPreviewUrlsCompletion('my-app', 'my-schema').subscribe(result => { completions = result; }); @@ -669,19 +652,20 @@ describe('SchemasService', () => { return { id: `id${id}`, + category: `schema-category${key}`, created: buildDate(id, 10), createdBy: `creator${id}`, + isPublished: id % 3 === 0, + isSingleton: false, lastModified: buildDate(id, 20), lastModifiedBy: `modifier${id}`, - version: key, name: `schema-name${key}`, - category: `schema-category${key}`, - type: id % 2 === 0 ? 'Default' : 'Singleton', - isPublished: id % 3 === 0, - properties: schemaPropertiesResponse(id, suffix), previewUrls: { Default: 'url', }, + properties: schemaPropertiesResponse(id, suffix), + type: id % 2 === 0 ? 'Default' : 'Singleton', + version: id, fields: [ { fieldId: 11, @@ -854,10 +838,9 @@ describe('SchemasService', () => { ], fieldsInLists: ['field1'], fieldsInReferences: ['field1'], - fieldRules: - [{ - field: 'field1', action: 'Hide', condition: 'a === 2', - }], + fieldRules: [ + { field: 'field1', action: 'Hide', condition: 'a === 2' }, + ], scripts: { query: '', create: '', @@ -875,69 +858,197 @@ describe('SchemasService', () => { function createSchemaProperties(id: number, suffix = '') { const key = `${id}${suffix}`; - return new SchemaPropertiesDto( - `label${key}`, - `hints${key}`, - `url/to/contents/${key}`, - `url/to/content/${key}`, - `url/to/editor/${key}`, - `url/to/list/${key}`, - id % 2 === 1, - [ + return new SchemaPropertiesDto({ + label: `label${key}`, + contentsSidebarUrl: `url/to/contents/${key}`, + contentSidebarUrl: `url/to/content/${key}`, + contentEditorUrl: `url/to/editor/${key}`, + contentsListUrl: `url/to/list/${key}`, + tags: [ `tags${key}`, ], - ); + validateOnPublish: id % 2 === 1, + hints: `hints${key}`, + }); } export function createSchema(id: number, suffix = '') { - const links: ResourceLinks = { - update: { method: 'PUT', href: `/schemas/${id}` }, - }; - const key = `${id}${suffix}`; - return new SchemaDto(links, - `id${id}`, - DateTime.parseISO(buildDate(id, 10)), `creator${id}`, - DateTime.parseISO(buildDate(id, 20)), `modifier${id}`, - new Version(key), - `schema-name${key}`, - `schema-category${key}`, - id % 2 === 0 ? 'Default' : 'Singleton', - id % 3 === 0, - createSchemaProperties(id, suffix), - [ - new RootFieldDto({}, 11, 'field11', createProperties('Array'), 'language', true, true, true, [ - new NestedFieldDto({}, 101, 'field101', createProperties('String'), 11, true, true, true), - new NestedFieldDto({}, 102, 'field102', createProperties('Number'), 11, true, true, true), - ]), - new RootFieldDto({}, 12, 'field12', createProperties('Assets'), 'language', true, true, true), - new RootFieldDto({}, 13, 'field13', createProperties('Boolean'), 'language', true, true, true), - new RootFieldDto({}, 14, 'field14', createProperties('Component'), 'language', true, true, true), - new RootFieldDto({}, 15, 'field15', createProperties('Components'), 'language', true, true, true), - new RootFieldDto({}, 16, 'field16', createProperties('DateTime'), 'language', true, true, true), - new RootFieldDto({}, 17, 'field17', createProperties('Geolocation'), 'language', true, true, true), - new RootFieldDto({}, 18, 'field18', createProperties('Json'), 'language', true, true, true), - new RootFieldDto({}, 19, 'field19', createProperties('Number'), 'language', true, true, true), - new RootFieldDto({}, 20, 'field20', createProperties('References'), 'language', true, true, true), - new RootFieldDto({}, 21, 'field21', createProperties('String'), 'language', true, true, true), - new RootFieldDto({}, 22, 'field22', createProperties('Tags'), 'language', true, true, true), - ], - ['field1'], - ['field1'], - [{ - field: 'field1', action: 'Hide', condition: 'a === 2', - }], - { + return new SchemaDto({ + id: `id${id}`, + category: `schema-category${key}`, + created: DateTime.parseISO(buildDate(id, 10)), + createdBy: `creator${id}`, + isPublished: id % 3 === 0, + isSingleton: false, + lastModified: DateTime.parseISO(buildDate(id, 20)), + lastModifiedBy: `modifier${id}`, + name: `schema-name${key}`, + previewUrls: { Default: 'url', }, - { + properties: createSchemaProperties(id, suffix), + type: id % 2 === 0 ? 'Default' : 'Singleton', + version: id + suffix.length, + fields: [ + new FieldDto({ + fieldId: 11, + name: 'field11', + isLocked: true, + isHidden: true, + isDisabled: true, + partitioning: 'language', + properties: new ArrayFieldPropertiesDto(), + nested: [ + new NestedFieldDto({ + fieldId: 101, + name: 'field101', + isLocked: true, + isHidden: true, + isDisabled: true, + properties: new StringFieldPropertiesDto(), + _links: {}, + }), + new NestedFieldDto({ + fieldId: 102, + name: 'field102', + isLocked: true, + isHidden: true, + isDisabled: true, + properties: new NumberFieldPropertiesDto(), + _links: {}, + }), + ], + _links: {}, + }), + new FieldDto({ + fieldId: 12, + name: 'field12', + isLocked: true, + isHidden: true, + isDisabled: true, + partitioning: 'language', + properties: new AssetsFieldPropertiesDto(), + _links: {}, + }), + new FieldDto({ + fieldId: 13, + name: 'field13', + isLocked: true, + isHidden: true, + isDisabled: true, + partitioning: 'language', + properties: new BooleanFieldPropertiesDto(), + _links: {}, + }), + new FieldDto({ + fieldId: 14, + name: 'field14', + isLocked: true, + isHidden: true, + isDisabled: true, + partitioning: 'language', + properties: new ComponentFieldPropertiesDto(), + _links: {}, + }), + new FieldDto({ + fieldId: 15, + name: 'field15', + isLocked: true, + isHidden: true, + isDisabled: true, + partitioning: 'language', + properties: new ComponentsFieldPropertiesDto(), + _links: {}, + }), + new FieldDto({ + fieldId: 16, + name: 'field16', + isLocked: true, + isHidden: true, + isDisabled: true, + partitioning: 'language', + properties: new DateTimeFieldPropertiesDto(), + _links: {}, + }), + new FieldDto({ + fieldId: 17, + name: 'field17', + isLocked: true, + isHidden: true, + isDisabled: true, + partitioning: 'language', + properties: new GeolocationFieldPropertiesDto(), + _links: {}, + }), + new FieldDto({ + fieldId: 18, + name: 'field18', + isLocked: true, + isHidden: true, + isDisabled: true, + partitioning: 'language', + properties: new JsonFieldPropertiesDto(), + _links: {}, + }), + new FieldDto({ + fieldId: 19, + name: 'field19', + isLocked: true, + isHidden: true, + isDisabled: true, + partitioning: 'language', + properties: new NumberFieldPropertiesDto(), + _links: {}, + }), + new FieldDto({ + fieldId: 20, + name: 'field20', + isLocked: true, + isHidden: true, + isDisabled: true, + partitioning: 'language', + properties: new ReferencesFieldPropertiesDto(), + _links: {}, + }), + new FieldDto({ + fieldId: 21, + name: 'field21', + isLocked: true, + isHidden: true, + isDisabled: true, + partitioning: 'language', + properties: new StringFieldPropertiesDto(), + _links: {}, + }), + new FieldDto({ + fieldId: 22, + name: 'field22', + isLocked: true, + isHidden: true, + isDisabled: true, + partitioning: 'language', + properties: new TagsFieldPropertiesDto(), + _links: {}, + }), + ], + fieldsInLists: ['field1'], + fieldsInReferences: ['field1'], + fieldRules: [ + new FieldRuleDto({ field: 'field1', action: 'Hide', condition: 'a === 2' }), + ], + scripts: new SchemaScriptsDto({ query: '', create: '', change: '', delete: '', update: '', - }); + }), + _links: { + update: new ResourceLinkDto({ method: 'PUT', href: `/schemas/${id}` }), + }, + }); } function buildDate(id: number, add = 0) { diff --git a/frontend/src/app/shared/services/schemas.service.ts b/frontend/src/app/shared/services/schemas.service.ts index e8985592f..7ad561b51 100644 --- a/frontend/src/app/shared/services/schemas.service.ts +++ b/frontend/src/app/shared/services/schemas.service.ts @@ -9,502 +9,9 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { ApiUrlConfig, DateTime, hasAnyLink, HTTP, pretifyError, Resource, ResourceLinks, ScriptCompletions, StringHelper, Types, Version, Versioned } from '@app/framework'; +import { ApiUrlConfig, HTTP, pretifyError, Resource, ScriptCompletions, Versioned, VersionOrTag } from '@app/framework'; +import { AddFieldDto, ChangeCategoryDto, ConfigureFieldRulesDto, ConfigureUIFieldsDto, CreateSchemaDto, SchemaDto, SchemasDto, SynchronizeSchemaDto, UpdateFieldDto, UpdateSchemaDto } from './../model'; import { QueryModel } from './query'; -import { createProperties, FieldPropertiesDto } from './schemas.types'; - -export type FieldRuleAction = 'Disable' | 'Hide' | 'Require'; -export type SchemaType = 'Default' | 'Singleton' | 'Component'; -export type SchemaScripts = Record; -export type PreviewUrls = Record; - -export const META_FIELDS = { - empty: { - name: '', - label: '', - title: '', - }, - id: { - name: 'id', - label: 'i18n:schemas.tableHeaders.id', - title: 'i18n:schemas.tableHeaders.id_title', - }, - created: { - name: 'created', - label: 'i18n:schemas.tableHeaders.created', - title: 'i18n:schemas.tableHeaders.created_title', - }, - createdByAvatar: { - name: 'createdBy.avatar', - label: 'i18n:schemas.tableHeaders.createdByShort', - title: 'i18n:schemas.tableHeaders.createdByShort_title', - }, - createdByName: { - name: 'createdBy.name', - label: 'i18n:schemas.tableHeaders.createdBy', - title: 'i18n:schemas.tableHeaders.createdBy_title', - }, - lastModified: { - name: 'lastModified', - label: 'i18n:schemas.tableHeaders.lastModified', - title: 'i18n:schemas.tableHeaders.lastModified_title', - }, - lastModifiedByAvatar: { - name: 'lastModifiedBy.avatar', - label: 'i18n:schemas.tableHeaders.lastModifiedByShort', - title: 'i18n:schemas.tableHeaders.lastModifiedByShort_title', - }, - lastModifiedByName: { - name: 'lastModifiedBy.name', - label: 'i18n:schemas.tableHeaders.lastModifiedBy', - title: 'i18n:schemas.tableHeaders.lastModifiedBy_title', - }, - status: { - name: 'status', - label: 'i18n:schemas.tableHeaders.status', - title: 'i18n:schemas.tableHeaders.status_title', - }, - statusColor: { - name: 'status.color', - label: 'i18n:schemas.tableHeaders.status', - title: 'i18n:schemas.tableHeaders.status_title', - }, - statusNext: { - name: 'status.next', - label: 'i18n:schemas.tableHeaders.nextStatus', - title: 'i18n:schemas.tableHeaders.nextStatus_title', - }, - version: { - name: 'version', - label: 'i18n:schemas.tableHeaders.version', - title: 'i18n:schemas.tableHeaders.version_title', - }, - translationStatus: { - name: 'translationStatus', - label: 'i18n:schemas.tableHeaders.translationStatus', - title: 'i18n:schemas.tableHeaders.translationStatus_title', - }, - translationStatusAverage: { - name: 'translationStatusAverage', - label: 'i18n:schemas.tableHeaders.translationStatusAverage', - title: 'i18n:schemas.tableHeaders.translationStatusAverage_title', - }, -}; - -export function getTableFields(fields: ReadonlyArray) { - const result: string[] = []; - - for (const field of fields) { - if (field.name?.startsWith('data.')) { - result.push(field.name); - } - } - - result.sort(); - - return result; -} - -export const FIELD_RULE_ACTIONS: ReadonlyArray = [ - 'Disable', - 'Hide', - 'Require', -]; - -export class SchemaDto { - public readonly _links: ResourceLinks; - - public readonly canAddField: boolean; - public readonly canContentsCreate: boolean; - public readonly canContentsCreateAndPublish: boolean; - public readonly canContentsRead: boolean; - public readonly canDelete: boolean; - public readonly canOrderFields: boolean; - public readonly canPublish: boolean; - public readonly canReadContents: boolean; - public readonly canSynchronize: boolean; - public readonly canUnpublish: boolean; - public readonly canUpdate: boolean; - public readonly canUpdateCategory: boolean; - public readonly canUpdateRules: boolean; - public readonly canUpdateScripts: boolean; - public readonly canUpdateUIFields: boolean; - public readonly canUpdateUrls: boolean; - - public readonly displayName: string; - - public readonly contentFields: ReadonlyArray = []; - - public readonly defaultListFields: ReadonlyArray = []; - public readonly defaultReferenceFields: ReadonlyArray = []; - - constructor(links: ResourceLinks, - public readonly id: string, - public readonly created: DateTime, - public readonly createdBy: string, - public readonly lastModified: DateTime, - public readonly lastModifiedBy: string, - public readonly version: Version, - public readonly name: string, - public readonly category: string, - public readonly type: SchemaType, - public readonly isPublished: boolean, - public readonly properties: SchemaPropertiesDto, - public readonly fields: ReadonlyArray = [], - public readonly fieldsInLists: ReadonlyArray = [], - public readonly fieldsInReferences: ReadonlyArray = [], - public readonly fieldRules: ReadonlyArray = [], - public readonly previewUrls: PreviewUrls = {}, - public readonly scripts: SchemaScripts = {}, - ) { - this._links = links; - - this.canAddField = hasAnyLink(links, 'fields/add'); - this.canContentsCreate = hasAnyLink(links, 'contents/create'); - this.canContentsCreateAndPublish = hasAnyLink(links, 'contents/create/publish'); - this.canContentsRead = hasAnyLink(links, 'contents'); - this.canDelete = hasAnyLink(links, 'delete'); - this.canOrderFields = hasAnyLink(links, 'fields/order'); - this.canPublish = hasAnyLink(links, 'publish'); - this.canReadContents = hasAnyLink(links, 'contents'); - this.canSynchronize = hasAnyLink(this, 'update/sync'); - this.canUnpublish = hasAnyLink(links, 'unpublish'); - this.canUpdate = hasAnyLink(links, 'update'); - this.canUpdateCategory = hasAnyLink(links, 'update/category'); - this.canUpdateRules = hasAnyLink(links, 'update/rules'); - this.canUpdateScripts = hasAnyLink(links, 'update/scripts'); - this.canUpdateUIFields = hasAnyLink(links, 'fields/ui'); - this.canUpdateUrls = hasAnyLink(links, 'update/urls'); - - this.displayName = StringHelper.firstNonEmpty(this.properties.label, this.name); - - function tableField(rootField: RootFieldDto) { - const label = rootField.displayName; - - return { name: `data.${rootField.name}`, label, rootField }; - } - - if (fields) { - this.contentFields = fields.filter(x => x.properties.isContentField).map(tableField); - - function tableFields(names: ReadonlyArray, fields: ReadonlyArray): TableField[] { - const result: TableField[] = []; - - for (const name of names) { - const metaField = Object.values(META_FIELDS).find(x => x.name === name); - - if (metaField) { - result.push(metaField); - } else { - const field = fields.find(x => x.name === name); - - if (field) { - result.push(field); - } - } - } - - return result; - } - - const listFields = tableFields(fieldsInLists, this.contentFields); - - if (listFields.length === 0) { - listFields.push(META_FIELDS.lastModifiedByAvatar); - - if (fields.length > 0) { - listFields.push(tableField(this.fields[0])); - } else { - listFields.push(META_FIELDS.empty); - } - - listFields.push(META_FIELDS.statusColor); - listFields.push(META_FIELDS.lastModified); - } - - this.defaultListFields = listFields; - - const referenceFields = tableFields(fieldsInReferences, this.contentFields); - - if (referenceFields.length === 0) { - if (fields.length > 0) { - referenceFields.push(tableField(this.fields[0])); - } else { - referenceFields.push(META_FIELDS.empty); - } - } - - this.defaultReferenceFields = referenceFields; - } - } - - public export(): any { - const fieldKeys = [ - 'fieldId', - 'parentId', - 'parentFieldId', - '_links', - ]; - - const cleanup = (source: any, ...exclude: string[]): any => { - const clone = {} as Record; - - for (const [key, value] of Object.entries(source)) { - if (!exclude.includes(key) && key.indexOf('can') !== 0 && !Types.isUndefined(value) && !Types.isNull(value)) { - clone[key] = value; - } - } - - return clone; - }; - - const result: any = { - previewUrls: this.previewUrls, - properties: cleanup(this.properties), - category: this.category, - scripts: this.scripts, - isPublished: this.isPublished, - fieldRules: this.fieldRules, - fieldsInLists: this.fieldsInLists, - fieldsInReferences: this.fieldsInReferences, - fields: this.fields.map(field => { - const copy = cleanup(field, ...fieldKeys); - - copy.properties = cleanup(field.properties); - - if (Types.isArray(copy.nested)) { - if (copy.nested.length === 0) { - delete copy['nested']; - } else { - copy.nested = field.nested.map(nestedField => { - const nestedCopy = cleanup(nestedField, ...fieldKeys); - - nestedCopy.properties = cleanup(nestedField.properties); - - return nestedCopy; - }); - } - } - - return copy; - }), - type: this.type, - }; - - return result; - } -} - -export class FieldDto { - public readonly _links: ResourceLinks; - - public readonly canAddField: boolean; - public readonly canDelete: boolean; - public readonly canDisable: boolean; - public readonly canEnable: boolean; - public readonly canHide: boolean; - public readonly canLock: boolean; - public readonly canOrderFields: boolean; - public readonly canShow: boolean; - public readonly canUpdate: boolean; - - public get isInlineEditable(): boolean { - return !this.isDisabled && this.rawProperties.inlineEditable === true; - } - - public get displayName() { - return StringHelper.firstNonEmpty(this.properties.label, this.name); - } - - public get displayPlaceholder() { - return this.properties.placeholder || ''; - } - - public get rawProperties(): any { - return this.properties; - } - - constructor(links: ResourceLinks, - public readonly fieldId: number, - public readonly name: string, - public readonly properties: FieldPropertiesDto, - public readonly isLocked: boolean = false, - public readonly isHidden: boolean = false, - public readonly isDisabled: boolean = false, - ) { - this._links = links; - - this.canAddField = hasAnyLink(links, 'fields/add'); - this.canDelete = hasAnyLink(links, 'delete'); - this.canDisable = hasAnyLink(links, 'disable'); - this.canEnable = hasAnyLink(links, 'enable'); - this.canOrderFields = hasAnyLink(links, 'fields/order'); - this.canHide = hasAnyLink(links, 'hide'); - this.canLock = hasAnyLink(links, 'lock'); - this.canShow = hasAnyLink(links, 'show'); - this.canUpdate = hasAnyLink(links, 'update'); - } -} - -export class RootFieldDto extends FieldDto { - public get isLocalizable() { - return this.partitioning === 'language'; - } - - constructor(links: ResourceLinks, fieldId: number, name: string, properties: FieldPropertiesDto, - public readonly partitioning: string, - isLocked: boolean = false, - isHidden: boolean = false, - isDisabled: boolean = false, - public readonly nested: ReadonlyArray = [], - ) { - super(links, fieldId, name, properties, isLocked, isHidden, isDisabled); - } -} - -export class NestedFieldDto extends FieldDto { - constructor(links: ResourceLinks, fieldId: number, name: string, properties: FieldPropertiesDto, - public readonly parentId: number, - isLocked: boolean = false, - isHidden: boolean = false, - isDisabled: boolean = false, - ) { - super(links, fieldId, name, properties, isLocked, isHidden, isDisabled); - } -} - -export class SchemaPropertiesDto { - constructor( - public readonly label?: string, - public readonly hints?: string, - public readonly contentsSidebarUrl?: string, - public readonly contentSidebarUrl?: string, - public readonly contentEditorUrl?: string, - public readonly contentsListUrl?: string, - public readonly validateOnPublish?: boolean, - public readonly tags?: ReadonlyArray, - ) { - } -} - -export type TableField = Readonly<{ - // The name of the table field. - name: string; - - // The label for the table header. - label: string; - - // The title. - title?: string; - - // The reference to the root field. - rootField?: RootFieldDto; -}>; - -export type FieldRule = Readonly<{ - // The path to the field to update when the rule is valid. - field: string; - - // The action to invoke. - action: FieldRuleAction; - - //The condition as javascript expression. - condition: string; -}>; - -export type SchemasDto = Readonly<{ - // The list of schemas. - items: ReadonlyArray; - - // True, if the user has permissions to create a new schema. - canCreate?: boolean; -}>; - -export type AddFieldDto = Readonly<{ - // The name of the field. - name: string; - - // The partitioning of the field. - partitioning?: string; - - // The field properties. - properties: FieldPropertiesDto; -}>; - -export type UpdateUIFields = Readonly<{ - // The names of all fields that should be shown in the list. - fieldsInLists?: ReadonlyArray; - - // The names of all fields that should be shown in the reference list. - fieldsInReferences?: ReadonlyArray; -}>; - -export type CreateSchemaDto = Readonly<{ - // The name of the schema. - name: string; - - // The initial fields of the schema. - fields?: ReadonlyArray; - - // The category name. - category?: string; - - // The type of the schema. - type?: string; - - // The initial published state. - isPublished?: boolean; - - // The initial schema properties. - properties?: SchemaPropertiesDto; -}>; - -export type UpdateSchemaCategoryDto = Readonly<{ - // The name of the category. - name?: string; -}>; - -export type UpdateFieldDto = Readonly<{ - // The field properties. - properties: FieldPropertiesDto; -}>; - -export type SynchronizeSchemaDto = Readonly<{ - // True, to not delete fields when synchronizing. - noFieldDeletiong?: boolean; - - // True, to not recreate fields when synchronizing. - noFieldRecreation?: boolean; - - // The additional properties. - [key: string]: any; -}>; - -export type UpdateSchemaDto = Readonly<{ - // The label of the schema. - label?: string; - - // The hints to explain the schema. - hints?: string; - - // The URL to the contents sidebar plugin. - contentsSidebarUrl?: string; - - // The URL to the content sidebar plugin. - contentSidebarUrl?: string; - - // The URL to an editor to replace the editor. - contentEditorUrl?: string; - - // The url to the content list plugin. - contentsListUrl?: string; - - // True, if the content should be validated on publishing. - validateOnPublish?: boolean; - - // The tags. - tags?: ReadonlyArray; -}>; @Injectable({ providedIn: 'root', @@ -521,7 +28,7 @@ export class SchemasService { return HTTP.getVersioned(this.http, url).pipe( map(({ payload }) => { - return parseSchemas(payload.body); + return SchemasDto.fromJSON(payload.body); }), pretifyError('i18n:schemas.loadFailed')); } @@ -531,7 +38,7 @@ export class SchemasService { return HTTP.getVersioned(this.http, url).pipe( map(({ payload }) => { - return parseSchema(payload.body); + return SchemaDto.fromJSON(payload.body); }), pretifyError('i18n:schemas.loadSchemaFailed')); } @@ -539,230 +46,230 @@ export class SchemasService { public postSchema(appName: string, dto: CreateSchemaDto): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas`); - return HTTP.postVersioned(this.http, url, dto).pipe( + return HTTP.postVersioned(this.http, url, dto.toJSON()).pipe( map(({ payload }) => { - return parseSchema(payload.body); + return SchemaDto.fromJSON(payload.body); }), pretifyError('i18n:schemas.createFailed')); } - public putScripts(appName: string, resource: Resource, dto: {}, version: Version): Observable { + public putScripts(appName: string, resource: Resource, dto: Record, version: VersionOrTag): Observable { const link = resource._links['update/scripts']; const url = this.apiUrl.buildUrl(link.href); return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe( map(({ payload }) => { - return parseSchema(payload.body); + return SchemaDto.fromJSON(payload.body); }), pretifyError('i18n:schemas.updateScriptsFailed')); } - public putFieldRules(appName: string, resource: Resource, dto: ReadonlyArray, version: Version): Observable { + public putFieldRules(appName: string, resource: Resource, dto: ConfigureFieldRulesDto, version: VersionOrTag): Observable { const link = resource._links['update/rules']; const url = this.apiUrl.buildUrl(link.href); - return HTTP.requestVersioned(this.http, link.method, url, version, { fieldRules: dto }).pipe( + return HTTP.requestVersioned(this.http, link.method, url, version, dto.toJSON()).pipe( map(({ payload }) => { - return parseSchema(payload.body); + return SchemaDto.fromJSON(payload.body); }), pretifyError('i18n:schemas.updateRulesFailed')); } - public putSchemaSync(appName: string, resource: Resource, dto: SynchronizeSchemaDto, version: Version): Observable { + public putSchemaSync(appName: string, resource: Resource, dto: SynchronizeSchemaDto, version: VersionOrTag): Observable { const link = resource._links['update/sync']; const url = this.apiUrl.buildUrl(link.href); - return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe( + return HTTP.requestVersioned(this.http, link.method, url, version, dto.toJSON()).pipe( map(({ payload }) => { - return parseSchema(payload.body); + return SchemaDto.fromJSON(payload.body); }), pretifyError('i18n:schemas.synchronizeFailed')); } - public putSchema(appName: string, resource: Resource, dto: UpdateSchemaDto, version: Version): Observable { + public putSchema(appName: string, resource: Resource, dto: UpdateSchemaDto, version: VersionOrTag): Observable { const link = resource._links['update']; const url = this.apiUrl.buildUrl(link.href); - return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe( + return HTTP.requestVersioned(this.http, link.method, url, version, new UpdateSchemaDto(dto)).pipe( map(({ payload }) => { - return parseSchema(payload.body); + return SchemaDto.fromJSON(payload.body); }), pretifyError('i18n:schemas.updateFailed')); } - public putCategory(appName: string, resource: Resource, dto: UpdateSchemaCategoryDto, version: Version): Observable { + public putCategory(appName: string, resource: Resource, dto: ChangeCategoryDto, version: VersionOrTag): Observable { const link = resource._links['update/category']; const url = this.apiUrl.buildUrl(link.href); - return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe( + return HTTP.requestVersioned(this.http, link.method, url, version, dto.toJSON()).pipe( map(({ payload }) => { - return parseSchema(payload.body); + return SchemaDto.fromJSON(payload.body); }), pretifyError('i18n:schemas.changeCategoryFailed')); } - public putPreviewUrls(appName: string, resource: Resource, dto: {}, version: Version): Observable { + public putPreviewUrls(appName: string, resource: Resource, dto: Record, version: VersionOrTag): Observable { const link = resource._links['update/urls']; const url = this.apiUrl.buildUrl(link.href); return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe( map(({ payload }) => { - return parseSchema(payload.body); + return SchemaDto.fromJSON(payload.body); }), pretifyError('i18n:schemas.updatePreviewUrlsFailed')); } - public publishSchema(appName: string, resource: Resource, version: Version): Observable { + public publishSchema(appName: string, resource: Resource, version: VersionOrTag): Observable { const link = resource._links['publish']; const url = this.apiUrl.buildUrl(link.href); return HTTP.requestVersioned(this.http, link.method, url, version, {}).pipe( map(({ payload }) => { - return parseSchema(payload.body); + return SchemaDto.fromJSON(payload.body); }), pretifyError('i18n:schemas.publishFailed')); } - public unpublishSchema(appName: string, resource: Resource, version: Version): Observable { + public unpublishSchema(appName: string, resource: Resource, version: VersionOrTag): Observable { const link = resource._links['unpublish']; const url = this.apiUrl.buildUrl(link.href); return HTTP.requestVersioned(this.http, link.method, url, version, {}).pipe( map(({ payload }) => { - return parseSchema(payload.body); + return SchemaDto.fromJSON(payload.body); }), pretifyError('i18n:schemas.unpublishFailed')); } - public postField(appName: string, resource: Resource, dto: AddFieldDto, version: Version): Observable { + public postField(appName: string, resource: Resource, dto: AddFieldDto, version: VersionOrTag): Observable { const link = resource._links['fields/add']; const url = this.apiUrl.buildUrl(link.href); - return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe( + return HTTP.requestVersioned(this.http, link.method, url, version, dto.toJSON()).pipe( map(({ payload }) => { - return parseSchema(payload.body); + return SchemaDto.fromJSON(payload.body); }), pretifyError('i18n:schemas.addFieldFailed')); } - public putUIFields(appName: string, resource: Resource, dto: UpdateUIFields, version: Version): Observable { + public putUIFields(appName: string, resource: Resource, dto: ConfigureUIFieldsDto, version: VersionOrTag): Observable { const link = resource._links['fields/ui']; const url = this.apiUrl.buildUrl(link.href); - return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe( + return HTTP.requestVersioned(this.http, link.method, url, version, dto.toJSON()).pipe( map(({ payload }) => { - return parseSchema(payload.body); + return SchemaDto.fromJSON(payload.body); }), pretifyError('i18n:schemas.updateUIFieldsFailed')); } - public putFieldOrdering(appName: string, resource: Resource, dto: ReadonlyArray, version: Version): Observable { + public putFieldOrdering(appName: string, resource: Resource, dto: ReadonlyArray, version: VersionOrTag): Observable { const link = resource._links['fields/order']; const url = this.apiUrl.buildUrl(link.href); return HTTP.requestVersioned(this.http, link.method, url, version, { fieldIds: dto }).pipe( map(({ payload }) => { - return parseSchema(payload.body); + return SchemaDto.fromJSON(payload.body); }), pretifyError('i18n:schemas.reorderFieldsFailed')); } - public putField(appName: string, resource: Resource, dto: UpdateFieldDto, version: Version): Observable { + public putField(appName: string, resource: Resource, dto: UpdateFieldDto, version: VersionOrTag): Observable { const link = resource._links['update']; const url = this.apiUrl.buildUrl(link.href); - return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe( + return HTTP.requestVersioned(this.http, link.method, url, version, dto.toJSON()).pipe( map(({ payload }) => { - return parseSchema(payload.body); + return SchemaDto.fromJSON(payload.body); }), pretifyError('i18n:schemas.updateFieldFailed')); } - public lockField(appName: string, resource: Resource, version: Version): Observable { + public lockField(appName: string, resource: Resource, version: VersionOrTag): Observable { const link = resource._links['lock']; const url = this.apiUrl.buildUrl(link.href); return HTTP.requestVersioned(this.http, link.method, url, version, {}).pipe( map(({ payload }) => { - return parseSchema(payload.body); + return SchemaDto.fromJSON(payload.body); }), pretifyError('i18n:schemas.lockFieldFailed')); } - public enableField(appName: string, resource: Resource, version: Version): Observable { + public enableField(appName: string, resource: Resource, version: VersionOrTag): Observable { const link = resource._links['enable']; const url = this.apiUrl.buildUrl(link.href); return HTTP.requestVersioned(this.http, link.method, url, version, {}).pipe( map(({ payload }) => { - return parseSchema(payload.body); + return SchemaDto.fromJSON(payload.body); }), pretifyError('i18n:schemas.enableFieldFailed')); } - public disableField(appName: string, resource: Resource, version: Version): Observable { + public disableField(appName: string, resource: Resource, version: VersionOrTag): Observable { const link = resource._links['disable']; const url = this.apiUrl.buildUrl(link.href); return HTTP.requestVersioned(this.http, link.method, url, version, {}).pipe( map(({ payload }) => { - return parseSchema(payload.body); + return SchemaDto.fromJSON(payload.body); }), pretifyError('i18n:schemas.disableFieldFailed')); } - public showField(appName: string, resource: Resource, version: Version): Observable { + public showField(appName: string, resource: Resource, version: VersionOrTag): Observable { const link = resource._links['show']; const url = this.apiUrl.buildUrl(link.href); return HTTP.requestVersioned(this.http, link.method, url, version, {}).pipe( map(({ payload }) => { - return parseSchema(payload.body); + return SchemaDto.fromJSON(payload.body); }), pretifyError('i18n:schemas.showFieldFailed')); } - public hideField(appName: string, resource: Resource, version: Version): Observable { + public hideField(appName: string, resource: Resource, version: VersionOrTag): Observable { const link = resource._links['hide']; const url = this.apiUrl.buildUrl(link.href); return HTTP.requestVersioned(this.http, link.method, url, version, {}).pipe( map(({ payload }) => { - return parseSchema(payload.body); + return SchemaDto.fromJSON(payload.body); }), pretifyError('i18n:schemas.hideFieldFailed')); } - public deleteField(appName: string, resource: Resource, version: Version): Observable { + public deleteField(appName: string, resource: Resource, version: VersionOrTag): Observable { const link = resource._links['delete']; const url = this.apiUrl.buildUrl(link.href); return HTTP.requestVersioned(this.http, link.method, url, version, {}).pipe( map(({ payload }) => { - return parseSchema(payload.body); + return SchemaDto.fromJSON(payload.body); }), pretifyError('i18n:schemas.deleteFieldFailed')); } - public deleteSchema(appName: string, resource: Resource, version: Version): Observable> { + public deleteSchema(appName: string, resource: Resource, version: VersionOrTag): Observable> { const link = resource._links['delete']; const url = this.apiUrl.buildUrl(link.href); @@ -800,83 +307,4 @@ export class SchemasService { return this.http.get(url); } -} - -function parseSchemas(response: { items: any[] } & Resource) { - const { items: list, _links } = response; - const items = list.map(parseSchema); - - const canCreate = hasAnyLink(_links, 'create'); - - return { items, canCreate }; -} - -function parseSchema(response: any) { - const fields = response.fields.map(parseField); - - return new SchemaDto(response._links, - response.id, - DateTime.parseISO(response.created), response.createdBy, - DateTime.parseISO(response.lastModified), response.lastModifiedBy, - new Version(response.version.toString()), - response.name, - response.category, - response.type, - response.isPublished, - parseProperties(response.properties), - fields, - response.fieldsInLists, - response.fieldsInReferences, - response.fieldRules, - response.previewUrls || {}, - response.scripts || {}); -} - -function parseProperties(response: any) { - return new SchemaPropertiesDto( - response.label, - response.hints, - response.contentsSidebarUrl, - response.contentSidebarUrl, - response.contentEditorUrl, - response.contentsListUrl, - response.validateOnPublish, - response.tags); -} - -export function parseField(item: any) { - const propertiesDto = - createProperties( - item.properties.fieldType, - item.properties); - - let nested: NestedFieldDto[] | null = null; - - if (item.nested && item.nested.length > 0) { - nested = item.nested.map((nestedItem: any) => { - const nestedPropertiesDto = - createProperties( - nestedItem.properties.fieldType, - nestedItem.properties); - - return new NestedFieldDto(nestedItem._links, - nestedItem.fieldId, - nestedItem.name, - nestedPropertiesDto, - item.fieldId, - nestedItem.isLocked, - nestedItem.isHidden, - nestedItem.isDisabled); - }); - } - - return new RootFieldDto(item._links, - item.fieldId, - item.name, - propertiesDto, - item.partitioning, - item.isLocked, - item.isHidden, - item.isDisabled, - nested || []); -} +} \ No newline at end of file diff --git a/frontend/src/app/shared/services/schemas.spec.ts b/frontend/src/app/shared/services/schemas.spec.ts index 7372c64cc..b6d2c49c4 100644 --- a/frontend/src/app/shared/services/schemas.spec.ts +++ b/frontend/src/app/shared/services/schemas.spec.ts @@ -19,26 +19,26 @@ describe('SchemaDto', () => { const field3 = createField({ properties: createProperties('Array', { label: 'My Field 3' }), id: 3 }); it('should return label as display name', () => { - const schema = createSchema({ properties: new SchemaPropertiesDto('Label') }); + const schema = createSchema({ properties: new SchemaPropertiesDto({ label: 'Label', validateOnPublish: true }) }); expect(schema.displayName).toBe('Label'); }); it('should return name as display name if label is undefined', () => { - const schema = createSchema({ properties: new SchemaPropertiesDto(undefined) }); + const schema = createSchema({ properties: new SchemaPropertiesDto({ label: undefined, validateOnPublish: true }) }); expect(schema.displayName).toBe('schema-name1'); }); it('should return name as display name label is empty', () => { - const schema = createSchema({ properties: new SchemaPropertiesDto('') }); + const schema = createSchema({ properties: new SchemaPropertiesDto({ label: '', validateOnPublish: true }) }); expect(schema.displayName).toBe('schema-name1'); }); it('should return configured fields as list fields if fields are declared', () => { const schema = createSchema({ - properties: new SchemaPropertiesDto(''), + properties: new SchemaPropertiesDto({ validateOnPublish: true }), fieldsInLists: [ 'data.field1', 'data.field3', @@ -56,7 +56,7 @@ describe('SchemaDto', () => { it('should return configured fields as references fields if fields are declared', () => { const schema = createSchema({ - properties: new SchemaPropertiesDto(''), + properties: new SchemaPropertiesDto(), fieldsInReferences: [ 'data.field1', 'data.field3', @@ -73,7 +73,7 @@ describe('SchemaDto', () => { }); it('should return first fields as list fields if no field is declared', () => { - const schema = createSchema({ properties: new SchemaPropertiesDto(''), fields: [field1, field2, field3] }); + const schema = createSchema({ properties: new SchemaPropertiesDto(), fields: [field1, field2, field3] }); expect(schema.defaultListFields).toEqual([ META_FIELDS.lastModifiedByAvatar, @@ -95,7 +95,7 @@ describe('SchemaDto', () => { }); it('should return first field as reference fields if no field is declared', () => { - const schema = createSchema({ properties: new SchemaPropertiesDto(''), fields: [field1, field2, field3] }); + const schema = createSchema({ properties: new SchemaPropertiesDto(), fields: [field1, field2, field3] }); expect(schema.defaultReferenceFields).toEqual([ { name: 'data.field1', rootField: field1, label: field1.displayName }, diff --git a/frontend/src/app/shared/services/schemas.types.ts b/frontend/src/app/shared/services/schemas.types.ts deleted file mode 100644 index 926c48236..000000000 --- a/frontend/src/app/shared/services/schemas.types.ts +++ /dev/null @@ -1,549 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. - */ - -export type FieldType = - 'Array' | - 'Assets' | - 'Boolean' | - 'Component' | - 'Components' | - 'DateTime' | - 'Json' | - 'Geolocation' | - 'Number' | - 'References' | - 'RichText' | - 'String' | - 'Tags' | - 'UI'; - -export const fieldTypes: ReadonlyArray<{ type: FieldType; description: string }> = [ - { - type: 'String', - description: 'i18n:schemas.fieldTypes.string.description', - }, { - type: 'Assets', - description: 'i18n:schemas.fieldTypes.assets.description', - }, { - type: 'Boolean', - description: 'i18n:schemas.fieldTypes.boolean.description', - }, { - type: 'Component', - description: 'i18n:schemas.fieldTypes.component.description', - }, { - type: 'Components', - description: 'i18n:schemas.fieldTypes.components.description', - }, { - type: 'DateTime', - description: 'i18n:schemas.fieldTypes.dateTime.description', - }, { - type: 'Geolocation', - description: 'i18n:schemas.fieldTypes.geolocation.description', - }, { - type: 'Json', - description: 'i18n:schemas.fieldTypes.json.description', - }, { - type: 'Number', - description: 'i18n:schemas.fieldTypes.number.description', - }, { - type: 'References', - description: 'i18n:schemas.fieldTypes.references.description', - }, { - type: 'RichText', - description: 'i18n:schemas.fieldTypes.richText.description', - }, { - type: 'Tags', - description: 'i18n:schemas.fieldTypes.tags.description', - }, { - type: 'Array', - description: 'i18n:schemas.fieldTypes.array.description', - }, { - type: 'UI', - description: 'i18n:schemas.fieldTypes.ui.description', - }, -]; - -export const fieldInvariant = 'iv'; - -export function createProperties(fieldType: FieldType, values?: any): FieldPropertiesDto { - let properties: FieldPropertiesDto; - - switch (fieldType) { - case 'Array': - properties = new ArrayFieldPropertiesDto(); - break; - case 'Assets': - properties = new AssetsFieldPropertiesDto(); - break; - case 'Boolean': - properties = new BooleanFieldPropertiesDto(); - break; - case 'Component': - properties = new ComponentFieldPropertiesDto(); - break; - case 'Components': - properties = new ComponentsFieldPropertiesDto(); - break; - case 'DateTime': - properties = new DateTimeFieldPropertiesDto(); - break; - case 'Geolocation': - properties = new GeolocationFieldPropertiesDto(); - break; - case 'Json': - properties = new JsonFieldPropertiesDto(); - break; - case 'Number': - properties = new NumberFieldPropertiesDto(); - break; - case 'References': - properties = new ReferencesFieldPropertiesDto(); - break; - case 'RichText': - properties = new RichTextFieldPropertiesDto(); - break; - case 'String': - properties = new StringFieldPropertiesDto(); - break; - case 'Tags': - properties = new TagsFieldPropertiesDto(); - break; - case 'UI': - properties = new UIFieldPropertiesDto(); - break; - default: - throw new Error(`Unknown field type ${fieldType}.`); - } - - if (values) { - Object.assign(properties, values); - } - - return properties; -} - -export interface FieldPropertiesVisitor { - visitArray(properties: ArrayFieldPropertiesDto): T; - - visitAssets(properties: AssetsFieldPropertiesDto): T; - - visitBoolean(properties: BooleanFieldPropertiesDto): T; - - visitComponent(properties: ComponentFieldPropertiesDto): T; - - visitComponents(properties: ComponentsFieldPropertiesDto): T; - - visitDateTime(properties: DateTimeFieldPropertiesDto): T; - - visitGeolocation(properties: GeolocationFieldPropertiesDto): T; - - visitJson(properties: JsonFieldPropertiesDto): T; - - visitNumber(properties: NumberFieldPropertiesDto): T; - - visitReferences(properties: ReferencesFieldPropertiesDto): T; - - visitRichText(properties: RichTextFieldPropertiesDto): T; - - visitString(properties: StringFieldPropertiesDto): T; - - visitTags(properties: TagsFieldPropertiesDto): T; - - visitUI(properties: UIFieldPropertiesDto): T; -} - -type DefaultValue = { [key: string]: T | undefined | null }; - -export abstract class FieldPropertiesDto { - public abstract fieldType: FieldType; - - public readonly editorUrl?: string; - public readonly hints?: string; - public readonly isRequired: boolean = false; - public readonly isRequiredOnPublish: boolean = false; - public readonly isHalfWidth: boolean = false; - public readonly label?: string; - public readonly placeholder?: string; - public readonly tags?: ReadonlyArray; - - public get isComplexUI() { - return true; - } - - public get isSortable() { - return true; - } - - public get isContentField() { - return true; - } - - public abstract accept(visitor: FieldPropertiesVisitor): T; -} - -export class ArrayFieldPropertiesDto extends FieldPropertiesDto { - public readonly fieldType = 'Array'; - - public readonly calculatedDefaultValue: 'EmptyArray' | 'Null' = 'EmptyArray'; - public readonly maxItems?: number; - public readonly minItems?: number; - public readonly uniqueFields?: ReadonlyArray; - - public accept(visitor: FieldPropertiesVisitor): T { - return visitor.visitArray(this); - } -} - -export type AssetPreviewMode = 'ImageAndFileName' | 'Image' | 'FileName'; - -export const ASSET_PREVIEW_MODES: ReadonlyArray = [ - 'ImageAndFileName', - 'Image', - 'FileName', -]; - -export class AssetsFieldPropertiesDto extends FieldPropertiesDto { - public readonly fieldType = 'Assets'; - - public readonly allowDuplicates?: boolean; - public readonly allowedExtensions?: ReadonlyArray; - public readonly aspectHeight?: number; - public readonly aspectWidth?: number; - public readonly defaultValue?: ReadonlyArray; - public readonly defaultValues?: DefaultValue>; - public readonly expectedType?: string; - public readonly folderId?: string; - public readonly maxHeight?: number; - public readonly maxItems?: number; - public readonly maxSize?: number; - public readonly maxWidth?: number; - public readonly minHeight?: number; - public readonly minItems?: number; - public readonly minSize?: number; - public readonly minWidth?: number; - public readonly previewFormat?: string; - public readonly previewMode: AssetPreviewMode = 'FileName'; - public readonly resolveFirst = false; - - public get isSortable() { - return false; - } - - public accept(visitor: FieldPropertiesVisitor): T { - return visitor.visitAssets(this); - } -} - -export type BooleanFieldEditor = 'Checkbox' | 'Toggle'; - -export const BOOLEAN_FIELD_EDITORS: ReadonlyArray = [ - 'Checkbox', - 'Toggle', -]; - -export class BooleanFieldPropertiesDto extends FieldPropertiesDto { - public readonly fieldType = 'Boolean'; - - public readonly defaultValue?: boolean; - public readonly defaultValues?: DefaultValue; - public readonly editor: BooleanFieldEditor = 'Checkbox'; - public readonly inlineEditable: boolean = false; - - public get isComplexUI() { - return false; - } - - public accept(visitor: FieldPropertiesVisitor): T { - return visitor.visitBoolean(this); - } -} - -export class ComponentFieldPropertiesDto extends FieldPropertiesDto { - public readonly fieldType = 'Component'; - - public readonly schemaIds?: ReadonlyArray; - - public get isComplexUI() { - return true; - } - - public accept(visitor: FieldPropertiesVisitor): T { - return visitor.visitComponent(this); - } -} - -export class ComponentsFieldPropertiesDto extends FieldPropertiesDto { - public readonly fieldType = 'Components'; - - public readonly calculatedDefaultValue: 'EmptyArray' | 'Null' = 'EmptyArray'; - public readonly schemaIds?: ReadonlyArray; - public readonly maxItems?: number; - public readonly minItems?: number; - public readonly uniqueFields?: ReadonlyArray; - - public get isComplexUI() { - return true; - } - - public accept(visitor: FieldPropertiesVisitor): T { - return visitor.visitComponents(this); - } -} - -export type DateTimeFieldEditor = 'DateTime' | 'Date'; - -export const DATETIME_FIELD_EDITORS: ReadonlyArray = [ - 'DateTime', - 'Date', -]; - -export class DateTimeFieldPropertiesDto extends FieldPropertiesDto { - public readonly fieldType = 'DateTime'; - - public readonly calculatedDefaultValue?: 'Now' | 'Today'; - public readonly defaultValue?: string; - public readonly defaultValues?: DefaultValue; - public readonly format?: string; - public readonly editor: DateTimeFieldEditor = 'DateTime'; - public readonly maxValue?: string; - public readonly minValue?: string; - - public get isComplexUI() { - return false; - } - - public accept(visitor: FieldPropertiesVisitor): T { - return visitor.visitDateTime(this); - } -} - -export type GeolocationFieldEditor = 'Map'; - -export class GeolocationFieldPropertiesDto extends FieldPropertiesDto { - public readonly fieldType = 'Geolocation'; - - public readonly editor: GeolocationFieldEditor = 'Map'; - - public get isSortable() { - return false; - } - - public accept(visitor: FieldPropertiesVisitor): T { - return visitor.visitGeolocation(this); - } -} - -export class JsonFieldPropertiesDto extends FieldPropertiesDto { - public readonly fieldType = 'Json'; - - public get isSortable() { - return false; - } - - public accept(visitor: FieldPropertiesVisitor): T { - return visitor.visitJson(this); - } -} - -export type NumberFieldEditor = 'Input' | 'Radio' | 'Dropdown' | 'Stars'; - -export const NUMBER_FIELD_EDITORS: ReadonlyArray = [ - 'Input', - 'Radio', - 'Dropdown', - 'Stars', -]; - -export class NumberFieldPropertiesDto extends FieldPropertiesDto { - public readonly fieldType = 'Number'; - - public readonly allowedValues?: ReadonlyArray; - public readonly defaultValue?: number; - public readonly defaultValues?: DefaultValue; - public readonly editor: NumberFieldEditor = 'Input'; - public readonly inlineEditable: boolean = false; - public readonly isUnique: boolean = false; - public readonly maxValue?: number; - public readonly minValue?: number; - - public get isComplexUI() { - return false; - } - - public accept(visitor: FieldPropertiesVisitor): T { - return visitor.visitNumber(this); - } -} - -export type ReferencesFieldEditor = 'List' | 'Dropdown' | 'Checkboxes' | 'Tags' | 'Input' | 'Radio'; - -export const REFERENCES_FIELD_EDITORS: ReadonlyArray = [ - 'List', - 'Dropdown', - 'Checkboxes', - 'Tags', - 'Input', - 'Radio', -]; - -export class ReferencesFieldPropertiesDto extends FieldPropertiesDto { - public readonly fieldType = 'References'; - - public readonly allowDuplicates?: boolean; - public readonly defaultValue?: ReadonlyArray; - public readonly defaultValues?: DefaultValue>; - public readonly editor: ReferencesFieldEditor = 'List'; - public readonly maxItems?: number; - public readonly minItems?: number; - public readonly mustBePublished?: boolean; - public readonly query?: string; - public readonly resolveReference?: boolean; - public readonly schemaIds?: ReadonlyArray; - - public get singleId() { - return this.schemaIds?.[0] || null; - } - - public get isSortable() { - return false; - } - - public accept(visitor: FieldPropertiesVisitor): T { - return visitor.visitReferences(this); - } -} - -export class RichTextFieldPropertiesDto extends FieldPropertiesDto { - public readonly fieldType = 'RichText'; - - public readonly classNames?: ReadonlyArray; - public readonly folderId?: string; - public readonly maxCharacters?: number; - public readonly maxLength?: number; - public readonly maxWords?: number; - public readonly minCharacters?: number; - public readonly minLength?: number; - public readonly minWords?: number; - public readonly schemaIds?: ReadonlyArray; - - public get isSortable() { - return false; - } - - public accept(visitor: FieldPropertiesVisitor): T { - return visitor.visitRichText(this); - } -} - -export type StringFieldEditor = 'Color' | 'Dropdown' | 'Html' | 'Input' | 'Markdown' | 'Radio' | 'RichText' | 'Slug' | 'StockPhoto' | 'TextArea'; -export type StringContentType = 'Unspecified' | 'Markdown' | 'Html'; - -export const STRING_FIELD_EDITORS: ReadonlyArray = [ - 'Input', - 'TextArea', - 'RichText', - 'Slug', - 'Markdown', - 'Dropdown', - 'Radio', - 'Html', - 'StockPhoto', - 'Color', -]; - -export const STRING_CONTENT_TYPES: ReadonlyArray = [ - 'Unspecified', - 'Markdown', - 'Html', -]; - -export class StringFieldPropertiesDto extends FieldPropertiesDto { - public readonly fieldType = 'String'; - - public readonly allowedValues?: ReadonlyArray; - public readonly classNames?: ReadonlyArray; - public readonly contentType?: StringContentType; - public readonly createEnum: boolean = false; - public readonly defaultValue?: string; - public readonly defaultValues?: DefaultValue; - public readonly editor: StringFieldEditor = 'Input'; - public readonly folderId?: string; - public readonly inlineEditable: boolean = false; - public readonly isEmbeddable: boolean = false; - public readonly isUnique: boolean = false; - public readonly maxCharacters?: number; - public readonly maxLength?: number; - public readonly maxWords?: number; - public readonly minCharacters?: number; - public readonly minLength?: number; - public readonly minWords?: number; - public readonly pattern?: string; - public readonly patternMessage?: string; - public readonly schemaIds?: ReadonlyArray; - - public get isComplexUI() { - return this.editor !== 'Input' && this.editor !== 'Color' && this.editor !== 'Radio' && this.editor !== 'Slug' && this.editor !== 'TextArea'; - } - - public accept(visitor: FieldPropertiesVisitor): T { - return visitor.visitString(this); - } -} - -export type TagsFieldEditor = 'Tags' | 'Checkboxes' | 'Dropdown'; - -export const TAGS_FIELD_EDITORS: ReadonlyArray = [ - 'Tags', - 'Checkboxes', - 'Dropdown', -]; - -export class TagsFieldPropertiesDto extends FieldPropertiesDto { - public readonly fieldType = 'Tags'; - - public readonly allowedValues?: ReadonlyArray; - public readonly createEnum: boolean = false; - public readonly defaultValue?: ReadonlyArray; - public readonly defaultValues?: DefaultValue>; - public readonly editor: TagsFieldEditor = 'Tags'; - public readonly maxItems?: number; - public readonly minItems?: number; - - public get isComplexUI() { - return false; - } - - public get isSortable() { - return false; - } - - public accept(visitor: FieldPropertiesVisitor): T { - return visitor.visitTags(this); - } -} - -export class UIFieldPropertiesDto extends FieldPropertiesDto { - public readonly fieldType = 'UI'; - - public readonly editor = 'Separator'; - - public get isComplexUI() { - return false; - } - - public get isSortable() { - return false; - } - - public get isContentField() { - return false; - } - - public accept(visitor: FieldPropertiesVisitor): T { - return visitor.visitUI(this); - } -} diff --git a/frontend/src/app/shared/services/search.service.spec.ts b/frontend/src/app/shared/services/search.service.spec.ts index a13969e72..ed2007d3e 100644 --- a/frontend/src/app/shared/services/search.service.spec.ts +++ b/frontend/src/app/shared/services/search.service.spec.ts @@ -8,7 +8,8 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { ApiUrlConfig, ResourceLinks, SearchResultDto, SearchService } from '@app/shared/internal'; +import { ApiUrlConfig, SearchResultDto, SearchService } from '@app/shared/internal'; +import { ResourceLinkDto } from './../model'; describe('SearchService', () => { beforeEach(() => { @@ -64,12 +65,12 @@ describe('SearchService', () => { }); export function createSearchResult(id: number) { - const links: ResourceLinks = { - url: { method: 'GET', href: `/url${id}` }, - }; - - return new SearchResultDto(links, - `Search Result ${id}`, - `Search Type ${id}`, - `Label ${id}`); + return new SearchResultDto({ + name: `Search Result ${id}`, + type: `Search Type ${id}` as any, + label: `Label ${id}`, + _links: { + url: new ResourceLinkDto({ method: 'GET', href: `/url${id}` }), + }, + }); } diff --git a/frontend/src/app/shared/services/search.service.ts b/frontend/src/app/shared/services/search.service.ts index e92f02e8a..99df6d252 100644 --- a/frontend/src/app/shared/services/search.service.ts +++ b/frontend/src/app/shared/services/search.service.ts @@ -9,23 +9,8 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { ApiUrlConfig, pretifyError, ResourceLinks, StringHelper } from '@app/framework'; - -export class SearchResultDto { - public readonly _links: ResourceLinks; - - public readonly url: string; - - constructor(links: ResourceLinks, - public readonly name: string, - public readonly type: string, - public readonly label?: string, - ) { - this._links = links; - - this.url = this._links['url'].href; - } -} +import { ApiUrlConfig, pretifyError, StringHelper } from '@app/framework'; +import { SearchResultDto } from './../model'; @Injectable({ providedIn: 'root', @@ -42,21 +27,8 @@ export class SearchService { return this.http.get(url).pipe( map(body => { - return parseResults(body); + return body.map(SearchResultDto.fromJSON); }), pretifyError('i18n:search.searchFailed')); } } - -function parseResults(response: any[]) { - return response.map(parseResult); -} - -function parseResult(response: any) { - return new SearchResultDto( - response._links, - response.name, - response.type, - response.label); -} - diff --git a/frontend/src/app/shared/services/shared.ts b/frontend/src/app/shared/services/shared.ts deleted file mode 100644 index 6df30a37f..000000000 --- a/frontend/src/app/shared/services/shared.ts +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. - */ - -import { hasAnyLink, Resource, ResourceLinks, Versioned } from '@app/framework'; - -export class ContributorDto { - public readonly _links: ResourceLinks; - - public readonly canRevoke: boolean; - public readonly canUpdate: boolean; - - public get token() { - return `subject:${this.contributorId}`; - } - - constructor(links: ResourceLinks, - public readonly contributorId: string, - public readonly contributorName: string, - public readonly contributorEmail: string, - public readonly role: string, - ) { - this._links = links; - - this.canRevoke = hasAnyLink(links, 'delete'); - this.canUpdate = hasAnyLink(links, 'update'); - } -} - -export type ContributorsDto = Versioned; - -export type ContributorsPayload = Readonly<{ - // The list of contributors. - items: ReadonlyArray; - - // The number of allowed contributors. - maxContributors: number; - - // True, if the user has been invited. - isInvited?: boolean; - - // True, if the user has permission to create a contributor. - canCreate?: boolean; -}>; - -export type AssignContributorDto = Readonly<{ - // The user ID. - contributorId: string; - - // The role for the contributor. - role: string; - - // True, if the user should be invited. - invite?: boolean; -}>; - -export class PlanDto { - constructor( - public readonly id: string, - public readonly name: string, - public readonly costs: string, - public readonly confirmText: string | undefined, - public readonly yearlyId: string, - public readonly yearlyCosts: string, - public readonly yearlyConfirmText: string | undefined, - public readonly maxApiBytes: number, - public readonly maxApiCalls: number, - public readonly maxAssetSize: number, - public readonly maxContributors: number, - ) { - } -} - -export type PlanLockedReason = 'None' | 'NotOwner' | 'NoPermission' | 'ManagedByTeam'; - -export type PlansDto = Versioned; - -export type PlansPayload = Readonly<{ - // The ID of the current plan. - currentPlanId: string; - - // The user, who owns the plan. - planOwner: string; - - // The actual plans. - plans: ReadonlyArray; - - // The portal link if available. - portalLink?: string; - - // The referral code. - referral?: ReferralDto; - - // The reason why the plan cannot be changed. - locked: PlanLockedReason; -}>; - -export type ReferralDto = Readonly<{ - // The referral code. - code: string; - - // The amount earned. - earned: string; - - // The referral condition. - condition: string; -}>; - -export type PlanChangedDto = Readonly<{ - // The redirect URI. - redirectUri?: string; -}>; - -export type ChangePlanDto = Readonly<{ - // The new plan ID. - planId: string; -}>; - -export function parsePlans(response: { plans: any[] } & any): PlansPayload { - const { plans: list, ...more } = response; - const plans = list.map(parsePlan); - - return { ...more, plans }; -} - -export function parsePlan(response: any) { - return new PlanDto( - response.id, - response.name, - response.costs, - response.confirmText, - response.yearlyId, - response.yearlyCosts, - response.yearlyConfirmText, - response.maxApiBytes, - response.maxApiCalls, - response.maxAssetSize, - response.maxContributors); -} - -export function parseContributors(response: { items: any[]; maxContributors: number } & Resource): ContributorsPayload { - const { items: list, maxContributors, _meta, _links } = response; - const items = list.map(parseContributor); - - const canCreate = hasAnyLink(_links, 'create'); - - return { items, maxContributors, canCreate, isInvited: _meta?.['isInvited'] === '1' }; -} - -export function parseContributor(response: any) { - return new ContributorDto(response._links, - response.contributorId, - response.contributorName, - response.contributorEmail, - response.role); -} \ No newline at end of file diff --git a/frontend/src/app/shared/services/teams.service.spec.ts b/frontend/src/app/shared/services/teams.service.spec.ts index f034214f0..2a1e76f2a 100644 --- a/frontend/src/app/shared/services/teams.service.spec.ts +++ b/frontend/src/app/shared/services/teams.service.spec.ts @@ -8,10 +8,11 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { ApiUrlConfig, DateTime, Resource, ResourceLinks, TeamAuthSchemeDto, TeamDto, TeamsService, Version } from '@app/shared/internal'; +import { ApiUrlConfig, AuthSchemeResponseDto, DateTime, Resource, TeamDto, TeamsService, Versioned, VersionTag } from '@app/shared/internal'; +import { AuthSchemeDto, AuthSchemeValueDto, CreateTeamDto, ResourceLinkDto, UpdateTeamDto } from '../model'; describe('TeamsService', () => { - const version = new Version('1'); + const version = new VersionTag('1'); beforeEach(() => { TestBed.configureTestingModule({ @@ -32,7 +33,6 @@ describe('TeamsService', () => { it('should make get request to get teams', inject([TeamsService, HttpTestingController], (teamsService: TeamsService, httpMock: HttpTestingController) => { let teams: ReadonlyArray; - teamsService.getTeams().subscribe(result => { teams = result; }); @@ -53,7 +53,6 @@ describe('TeamsService', () => { it('should make get request to get team', inject([TeamsService, HttpTestingController], (teamsService: TeamsService, httpMock: HttpTestingController) => { let team: TeamDto; - teamsService.getTeam('my-team').subscribe(result => { team = result; }); @@ -70,10 +69,9 @@ describe('TeamsService', () => { it('should make post request to create team', inject([TeamsService, HttpTestingController], (teamsService: TeamsService, httpMock: HttpTestingController) => { - const dto = { name: 'new-team' }; + const dto = new CreateTeamDto({ name: 'new-team' }); let team: TeamDto; - teamsService.postTeam(dto).subscribe(result => { team = result; }); @@ -90,6 +88,8 @@ describe('TeamsService', () => { it('should make put request to update team', inject([TeamsService, HttpTestingController], (teamsService: TeamsService, httpMock: HttpTestingController) => { + const dto = new UpdateTeamDto({ name: 'NewName' }); + const resource: Resource = { _links: { update: { method: 'PUT', href: '/api/teams/my-team' }, @@ -97,8 +97,7 @@ describe('TeamsService', () => { }; let team: TeamDto; - - teamsService.putTeam('my-team', resource, { }, version).subscribe(result => { + teamsService.putTeam('my-team', resource, dto, version).subscribe(result => { team = result; }); @@ -114,8 +113,7 @@ describe('TeamsService', () => { it('should make get request to get auth', inject([TeamsService, HttpTestingController], (teamsService: TeamsService, httpMock: HttpTestingController) => { - let auth: TeamAuthSchemeDto; - + let auth: Versioned; teamsService.getTeamAuth('my-team').subscribe(result => { auth = result; }); @@ -131,14 +129,15 @@ describe('TeamsService', () => { }, }); - expect(auth!).toEqual({ payload: { scheme: null, canUpdate: true }, version: new Version('2') }); + expect(auth!).toEqual({ payload: createTeamAuth(12), version: new VersionTag('2') }); })); it('should make put request to update auth', inject([TeamsService, HttpTestingController], (teamsService: TeamsService, httpMock: HttpTestingController) => { - let auth: TeamAuthSchemeDto; + const dto = new AuthSchemeValueDto({}); - teamsService.putTeamAuth('my-team', { scheme: null }, version).subscribe(result => { + let auth: Versioned; + teamsService.putTeamAuth('my-team', dto, version).subscribe(result => { auth = result; }); @@ -153,7 +152,7 @@ describe('TeamsService', () => { }, }); - expect(auth!).toEqual({ payload: { scheme: null, canUpdate: true }, version: new Version('2') }); + expect(auth!).toEqual({ payload: createTeamAuth(12), version: new VersionTag('2') }); })); it('should make delete request to leave team', @@ -197,14 +196,13 @@ describe('TeamsService', () => { return { id: `id${id}`, + name: `team-name${key}`, created: buildDate(id, 10), createdBy: `creator${id}`, lastModified: buildDate(id, 20), lastModifiedBy: `modifier${id}`, - version: key, - name: `team-name${key}`, roleName: `Role${id}`, - roleProperties: createProperties(id), + version: id, _links: { update: { method: 'PUT', href: `teams/${id}` }, }, @@ -212,9 +210,14 @@ describe('TeamsService', () => { } function teamAuthResponse(id: number) { - return { - scheme: null, + scheme: { + displayName: 'Auth', + domain: 'squidex.io', + clientId: 'client-id', + clientSecret: 'client-secret', + authority: 'squidex.io/authority', + }, _links: { update: { method: 'PUT', href: `teams/${id}/auth` }, }, @@ -223,28 +226,36 @@ describe('TeamsService', () => { }); export function createTeam(id: number, suffix = '') { - const links: ResourceLinks = { - update: { method: 'PUT', href: `teams/${id}` }, - }; - const key = `${id}${suffix}`; - return new TeamDto(links, - `id${id}`, - DateTime.parseISO(buildDate(id, 10)), `creator${id}`, - DateTime.parseISO(buildDate(id, 20)), `modifier${id}`, - new Version(key), - `team-name${key}`, - `Role${id}`, - createProperties(id)); + return new TeamDto({ + id: `id${id}`, + name: `team-name${key}`, + created: DateTime.parseISO(buildDate(id, 10)), + createdBy: `creator${id}`, + lastModified: DateTime.parseISO(buildDate(id, 20)), + lastModifiedBy: `modifier${id}`, + roleName: `Role${id}`, + version: id, + _links: { + update: new ResourceLinkDto({ method: 'PUT', href: `teams/${id}` }), + }, + }); } -function createProperties(id: number) { - const result = {} as Record; - - result[`property${id}`] = true; - - return result; +export function createTeamAuth(id: number) { + return new AuthSchemeResponseDto({ + scheme: new AuthSchemeDto({ + displayName: 'Auth', + domain: 'squidex.io', + clientId: 'client-id', + clientSecret: 'client-secret', + authority: 'squidex.io/authority', + }), + _links: { + update: new ResourceLinkDto({ method: 'PUT', href: `teams/${id}/auth` }), + }, + }); } function buildDate(id: number, add = 0) { diff --git a/frontend/src/app/shared/services/teams.service.ts b/frontend/src/app/shared/services/teams.service.ts index 193898443..df22e3bfc 100644 --- a/frontend/src/app/shared/services/teams.service.ts +++ b/frontend/src/app/shared/services/teams.service.ts @@ -9,82 +9,8 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { ApiUrlConfig, DateTime, hasAnyLink, HTTP, mapVersioned, pretifyError, Resource, ResourceLinks, Version, Versioned } from '@app/framework'; - -export class TeamDto { - public readonly _links: ResourceLinks; - - public readonly canDelete: boolean; - public readonly canReadAuth: boolean; - public readonly canReadContributors: boolean; - public readonly canReadPlans: boolean; - public readonly canUpdateGeneral: boolean; - - constructor(links: ResourceLinks, - public readonly id: string, - public readonly created: DateTime, - public readonly createdBy: string, - public readonly lastModified: DateTime, - public readonly lastModifiedBy: string, - public readonly version: Version, - public readonly name: string, - public readonly roleName: string, - public readonly roleProperties: {}, - ) { - this._links = links; - - this.canDelete = hasAnyLink(links, 'delete'); - this.canReadAuth = hasAnyLink(links, 'auth'); - this.canReadContributors = hasAnyLink(links, 'contributors'); - this.canReadPlans = hasAnyLink(links, 'plans'); - this.canUpdateGeneral = hasAnyLink(links, 'update'); - } -} - -export type TeamAuthSchemeDto = Versioned; - -export type TeamAuthSchemePayload = { - // The actual scheme. - scheme?: AuthSchemeDto | null; - - // True, when the scheme can be updated. - canUpdate: boolean; -}; - -export type AuthSchemeDto = Readonly<{ - // The domain name of your user accounts. - domain: string; - - // The display name for buttons. - displayName: string; - - // The client ID. - clientId: string; - - // The client secret. - clientSecret: string; - - // The authority URL. - authority: string; - - // The URL to redirect after a signout. - signoutRedirectUrl?: string; -}>; - -export type CreateTeamDto = Readonly<{ - // The new name of the team. Must not be unique. - name: string; -}>; - -export type UpdateTeamDto = Readonly<{ - // The new name of the team. Must not be unique. - name?: string; -}>; - -export type AuthSchemeValueDto = Readonly<{ - // The auth scheme. - scheme: AuthSchemeDto | null; -}>; +import { ApiUrlConfig, HTTP, mapVersioned, pretifyError, Resource, Versioned, VersionOrTag } from '@app/framework'; +import { AuthSchemeResponseDto, AuthSchemeValueDto, CreateTeamDto, TeamDto, UpdateTeamDto } from '../model'; @Injectable({ providedIn: 'root', @@ -101,9 +27,7 @@ export class TeamsService { return this.http.get(url).pipe( map(body => { - const teams = body.map(parseTeam); - - return teams; + return body.map(TeamDto.fromJSON); }), pretifyError('i18n:teams.loadFailed')); } @@ -113,9 +37,7 @@ export class TeamsService { return this.http.get(url).pipe( map(body => { - const team = parseTeam(body); - - return team; + return TeamDto.fromJSON(body); }), pretifyError('i18n:teams.teamLoadFailed')); } @@ -123,41 +45,41 @@ export class TeamsService { public postTeam(dto: CreateTeamDto): Observable { const url = this.apiUrl.buildUrl('api/teams'); - return this.http.post(url, dto).pipe( + return this.http.post(url, dto.toJSON()).pipe( map(body => { - return parseTeam(body); + return TeamDto.fromJSON(body); }), pretifyError('i18n:teams.createFailed')); } - public putTeam(teamId: string, resource: Resource, dto: UpdateTeamDto, version: Version): Observable { + public putTeam(teamId: string, resource: Resource, dto: UpdateTeamDto, version: VersionOrTag): Observable { const link = resource._links['update']; const url = this.apiUrl.buildUrl(link.href); - return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe( + return HTTP.requestVersioned(this.http, link.method, url, version, dto.toJSON()).pipe( map(({ payload }) => { - return parseTeam(payload.body); + return TeamDto.fromJSON(payload.body); }), pretifyError('i18n:teams.updateFailed')); } - public getTeamAuth(teamId: string): Observable { + public getTeamAuth(teamId: string): Observable> { const url = this.apiUrl.buildUrl(`api/teams/${teamId}/auth`); return HTTP.getVersioned(this.http, url).pipe( mapVersioned((payload) => { - return parseTeamAuth(payload.body); + return AuthSchemeResponseDto.fromJSON(payload.body); }), pretifyError('i18n:teams.teamLoadFailed')); } - public putTeamAuth(teamId: string, dto: AuthSchemeValueDto, version: Version): Observable { + public putTeamAuth(teamId: string, dto: AuthSchemeValueDto, version: VersionOrTag): Observable> { const url = this.apiUrl.buildUrl(`api/teams/${teamId}/auth`); - return HTTP.putVersioned(this.http, url, dto, version).pipe( + return HTTP.putVersioned(this.http, url, dto.toJSON(), version).pipe( mapVersioned((payload) => { - return parseTeamAuth(payload.body); + return AuthSchemeResponseDto.fromJSON(payload.body); }), pretifyError('i18n:teams.teamLoadFailed')); } @@ -179,23 +101,4 @@ export class TeamsService { return this.http.request(link.method, url).pipe( pretifyError('i18n:teams.archiveFailed')); } -} - -function parseTeam(response: any & Resource) { - return new TeamDto(response._links, - response.id, - DateTime.parseISO(response.created), response.createdBy, - DateTime.parseISO(response.lastModified), response.lastModifiedBy, - new Version(response.version.toString()), - response.name, - response.roleName, - response.roleProperties); -} - -function parseTeamAuth(response: any & Resource) { - const { scheme, _links } = response; - - const canUpdate = hasAnyLink(_links, 'update'); - - return { scheme, canUpdate }; } \ No newline at end of file diff --git a/frontend/src/app/shared/services/templates.service.spec.ts b/frontend/src/app/shared/services/templates.service.spec.ts index e49ecac0d..7f07385ce 100644 --- a/frontend/src/app/shared/services/templates.service.spec.ts +++ b/frontend/src/app/shared/services/templates.service.spec.ts @@ -8,7 +8,8 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { ApiUrlConfig, Resource, ResourceLinks, TemplateDetailsDto, TemplateDto, TemplatesDto, TemplatesService } from '@app/shared/internal'; +import { ApiUrlConfig, Resource, TemplateDetailsDto, TemplateDto, TemplatesDto, TemplatesService } from '@app/shared/internal'; +import { ResourceLinkDto } from '../model'; describe('TemplatesService', () => { beforeEach(() => { @@ -45,14 +46,16 @@ describe('TemplatesService', () => { templateResponse(1), templateResponse(2), ], + _links: {}, }); - expect(templates!).toEqual({ + expect(templates!).toEqual(new TemplatesDto({ items: [ createTemplate(1), createTemplate(2), ], - }); + _links: {}, + })); })); it('should make get request to get template', @@ -88,9 +91,7 @@ describe('TemplatesService', () => { description: `Description ${key}`, isStarter: id % 2 === 0, _links: { - self: { - method: 'GET', href: `/templates/name${key}`, - }, + self: { method: 'GET', href: `/templates/name${key}` }, }, }; } @@ -101,31 +102,34 @@ describe('TemplatesService', () => { return { details: `Details ${key}`, _links: { - self: { - method: 'GET', href: `/templates/name${id}`, - }, + self: { method: 'GET', href: `/templates/name${id}` }, }, }; } }); export function createTemplate(id: number, suffix = '') { - const links: ResourceLinks = { - self: { method: 'GET', href: `/templates/name${id}` }, - }; - const key = `${id}${suffix}`; - return new TemplateDto(links, `name${key}`, `Title ${key}`, `Description ${key}`, id % 2 === 0); + return new TemplateDto({ + name: `name${key}`, + title: `Title ${key}`, + description: `Description ${key}`, + isStarter: id % 2 === 0, + _links: { + self: new ResourceLinkDto({ method: 'GET', href: `/templates/name${key}` }), + }, + }); } export function createTemplateDetails(id: number, suffix = '') { - const links: ResourceLinks = { - self: { method: 'GET', href: `/templates/name${id}` }, - }; - const key = `${id}${suffix}`; - return new TemplateDetailsDto(links, `Details ${key}`); + return new TemplateDetailsDto({ + details: `Details ${key}`, + _links: { + self: new ResourceLinkDto({ method: 'GET', href: `/templates/name${id}` }), + }, + }); } \ No newline at end of file diff --git a/frontend/src/app/shared/services/templates.service.ts b/frontend/src/app/shared/services/templates.service.ts index 61a323ba6..8301992a6 100644 --- a/frontend/src/app/shared/services/templates.service.ts +++ b/frontend/src/app/shared/services/templates.service.ts @@ -9,35 +9,8 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { ApiUrlConfig, pretifyError, Resource, ResourceLinks } from '@app/framework'; - -export class TemplateDto { - public readonly _links: ResourceLinks; - - constructor(links: ResourceLinks, - public readonly name: string, - public readonly title: string, - public readonly description: string, - public readonly isStarter: boolean, - ) { - this._links = links; - } -} - -export class TemplateDetailsDto { - public readonly _links: ResourceLinks; - - constructor(links: ResourceLinks, - public readonly details: string, - ) { - this._links = links; - } -} - -export type TemplatesDto = Readonly<{ - // The list of templates. - items: ReadonlyArray; -}>; +import { ApiUrlConfig, pretifyError, Resource } from '@app/framework'; +import { TemplateDetailsDto, TemplatesDto } from './../model'; @Injectable({ providedIn: 'root', @@ -54,7 +27,7 @@ export class TemplatesService { return this.http.get(url).pipe( map(body => { - return parseTemplates(body); + return TemplatesDto.fromJSON(body); }), pretifyError('i18n:templates.loadFailed')); } @@ -66,28 +39,8 @@ export class TemplatesService { return this.http.get(url).pipe( map(body => { - return parseTemplateDetails(body); + return TemplateDetailsDto.fromJSON(body); }), pretifyError('i18n:templates.loadFailed')); } -} - -function parseTemplates(response: { items: any[] } & Resource): TemplatesDto { - const { items: list } = response; - const items = list.map(parseTemplate); - - return { items }; -} - -function parseTemplate(response: any & Resource) { - return new TemplateDto(response._links, - response.name, - response.title, - response.description, - response.isStarter); -} - -function parseTemplateDetails(response: any & Resource) { - return new TemplateDetailsDto(response._links, - response.details); } \ No newline at end of file diff --git a/frontend/src/app/shared/services/translations.service.spec.ts b/frontend/src/app/shared/services/translations.service.spec.ts index 45effeb3f..ccbebfdc3 100644 --- a/frontend/src/app/shared/services/translations.service.spec.ts +++ b/frontend/src/app/shared/services/translations.service.spec.ts @@ -8,7 +8,7 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { ApiUrlConfig, TranslationDto, TranslationsService } from '@app/shared/internal'; +import { ApiUrlConfig, TranslateDto, TranslationDto, TranslationsService } from '@app/shared/internal'; describe('TranslationsService', () => { beforeEach(() => { @@ -29,10 +29,9 @@ describe('TranslationsService', () => { it('should make post request to translate text', inject([TranslationsService, HttpTestingController], (translationsService: TranslationsService, httpMock: HttpTestingController) => { - const dto = { text: 'Hello', sourceLanguage: 'en', targetLanguage: 'de' }; + const dto = new TranslateDto({ text: 'Hello', sourceLanguage: 'en', targetLanguage: 'de' }); let translation: TranslationDto; - translationsService.translate('my-app', dto).subscribe(result => { translation = result; }); @@ -43,9 +42,15 @@ describe('TranslationsService', () => { expect(req.request.headers.get('If-Match')).toBeNull(); req.flush({ - text: 'Hallo', result: 'Translated', + result: 'Translated', + status: 'Translated', + text: 'Hallo', }); - expect(translation!).toEqual(new TranslationDto('Translated', 'Hallo')); + expect(translation!).toEqual(new TranslationDto({ + result: 'Translated', + status: 'Translated', + text: 'Hallo', + })); })); }); diff --git a/frontend/src/app/shared/services/translations.service.ts b/frontend/src/app/shared/services/translations.service.ts index c23141bc4..1b1636cbe 100644 --- a/frontend/src/app/shared/services/translations.service.ts +++ b/frontend/src/app/shared/services/translations.service.ts @@ -10,27 +10,9 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { ApiUrlConfig, pretifyError, StringHelper } from '@app/framework'; +import { TranslateDto, TranslationDto } from './../model'; import { AuthService } from './auth.service'; -export class TranslationDto { - constructor( - public readonly result: string, - public readonly text: string, - ) { - } -} - -export type TranslateDto = Readonly<{ - // The text to translate. - text: string; - - // The source language. - sourceLanguage: string; - - // The target language. - targetLanguage: string; - }>; - export type AskDto = Readonly<{ // Optional conversation ID. conversationId?: string; @@ -77,20 +59,20 @@ export class TranslationsService { ) { } - public translate(appName: string, request: TranslateDto): Observable { + public translate(appName: string, dto: TranslateDto): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/translations`); - return this.http.post(url, request).pipe( + return this.http.post(url, dto.toJSON()).pipe( map(body => { - return parseTranslation(body); + return TranslationDto.fromJSON(body); }), pretifyError('i18n:translate.translateFailed')); } - public ask(appName: string, request: AskDto): Observable { + public ask(appName: string, dto: AskDto): Observable { const token = this.authService.user!.accessToken; - const url = this.apiUrl.buildUrl(`api/apps/${appName}/ask${StringHelper.buildQuery({ ...request, access_token: token })}`); + const url = this.apiUrl.buildUrl(`api/apps/${appName}/ask${StringHelper.buildQuery({ ...dto, access_token: token })}`); return new Observable((subscriber) => { const source = new EventSource(url); @@ -127,9 +109,4 @@ export class TranslationsService { }; }); } -} - -function parseTranslation(body: any): TranslationDto { - return new TranslationDto(body.result, body.text); -} - +} \ No newline at end of file diff --git a/frontend/src/app/shared/services/usages.service.spec.ts b/frontend/src/app/shared/services/usages.service.spec.ts index 8fe3ea993..4a4f6ba5f 100644 --- a/frontend/src/app/shared/services/usages.service.spec.ts +++ b/frontend/src/app/shared/services/usages.service.spec.ts @@ -30,7 +30,6 @@ describe('UsagesService', () => { it('should make get request to get calls usages', inject([UsagesService, HttpTestingController], (usagesService: UsagesService, httpMock: HttpTestingController) => { let usages: CallsUsageDto; - usagesService.getCallsUsages('my-app', '2017-10-12', '2017-10-13').subscribe(result => { usages = result; }); @@ -48,7 +47,6 @@ describe('UsagesService', () => { it('should make get request to get calls usages for team', inject([UsagesService, HttpTestingController], (usagesService: UsagesService, httpMock: HttpTestingController) => { let usages: CallsUsageDto; - usagesService.getCallsUsagesForTeam('my-team', '2017-10-12', '2017-10-13').subscribe(result => { usages = result; }); @@ -66,7 +64,6 @@ describe('UsagesService', () => { it('should make get request to get storage usages', inject([UsagesService, HttpTestingController], (usagesService: UsagesService, httpMock: HttpTestingController) => { let usages: ReadonlyArray; - usagesService.getStorageUsages('my-app', '2017-10-12', '2017-10-13').subscribe(result => { usages = result; }); @@ -84,7 +81,6 @@ describe('UsagesService', () => { it('should make get request to get storage usages for team', inject([UsagesService, HttpTestingController], (usagesService: UsagesService, httpMock: HttpTestingController) => { let usages: ReadonlyArray; - usagesService.getStorageUsagesForTeam('my-team', '2017-10-12', '2017-10-13').subscribe(result => { usages = result; }); @@ -102,7 +98,6 @@ describe('UsagesService', () => { it('should make get request to get today storage', inject([UsagesService, HttpTestingController], (usagesService: UsagesService, httpMock: HttpTestingController) => { let usages: CurrentStorageDto; - usagesService.getTodayStorage('my-app').subscribe(result => { usages = result; }); @@ -114,7 +109,7 @@ describe('UsagesService', () => { req.flush({ size: 130, maxAllowed: 150 }); - expect(usages!).toEqual(new CurrentStorageDto(130, 150)); + expect(usages!).toEqual(new CurrentStorageDto({ size: 130, maxAllowed: 150 })); })); it('should make get request to get today storage for team', @@ -132,7 +127,7 @@ describe('UsagesService', () => { req.flush({ size: 130, maxAllowed: 150 }); - expect(usages!).toEqual(new CurrentStorageDto(130, 150)); + expect(usages!).toEqual(new CurrentStorageDto({ size: 130, maxAllowed: 150 })); })); it('should make get request to get log', @@ -152,64 +147,92 @@ describe('UsagesService', () => { expect(url!).toEqual('download/url'); })); + + function callsUsageResponse() { + return { + allowedBytes: 512, + allowedCalls: 100, + averageElapsedMs: 12.4, + blockingApiCalls: 200, + monthBytes: 256, + monthCalls: 5120, + totalBytes: 1024, + totalCalls: 40, + details: { + category1: [ + { + date: '2017-10-12', + totalBytes: 10, + totalCalls: 130, + averageElapsedMs: 12.3, + }, + { + date: '2017-10-13', + totalBytes: 13, + totalCalls: 170, + averageElapsedMs: 33.3, + }, + ], + }, + }; + } + + function storageUsageResponse() { + return [ + { + date: '2017-10-12', + totalCount: 10, + totalSize: 130, + }, + { + date: '2017-10-13', + totalCount: 13, + totalSize: 170, + }, + ]; + } }); -function callsUsageResponse() { - return { +export function callsUsageResult() { + return new CallsUsageDto({ allowedBytes: 512, allowedCalls: 100, - blockingCalls: 200, + averageElapsedMs: 12.4, + blockingApiCalls: 200, + monthBytes: 256, + monthCalls: 5120, totalBytes: 1024, totalCalls: 40, - monthCalls: 5120, - monthBytes: 256, - averageElapsedMs: 12.4, details: { category1: [ - { - date: '2017-10-12', + new CallsUsagePerDateDto({ + date: DateTime.parseISO('2017-10-12'), totalBytes: 10, totalCalls: 130, averageElapsedMs: 12.3, - }, - { - date: '2017-10-13', + }), + new CallsUsagePerDateDto({ + date: DateTime.parseISO('2017-10-13'), totalBytes: 13, totalCalls: 170, averageElapsedMs: 33.3, - }, + }), ], }, - }; -} - -function callsUsageResult() { - return new CallsUsageDto(512, 100, 200, 1024, 40, 256, 5120, 12.4, { - category1: [ - new CallsUsagePerDateDto(DateTime.parseISO('2017-10-12'), 10, 130, 12.3), - new CallsUsagePerDateDto(DateTime.parseISO('2017-10-13'), 13, 170, 33.3), - ], }); } -function storageUsageResponse() { +function storageUsageResult() { return [ - { - date: '2017-10-12', + new StorageUsagePerDateDto({ + date: DateTime.parseISO('2017-10-12'), totalCount: 10, totalSize: 130, - }, - { - date: '2017-10-13', + }), + new StorageUsagePerDateDto({ + date: DateTime.parseISO('2017-10-13'), totalCount: 13, totalSize: 170, - }, - ]; -} - -function storageUsageResult() { - return [ - new StorageUsagePerDateDto(DateTime.parseISO('2017-10-12'), 10, 130), - new StorageUsagePerDateDto(DateTime.parseISO('2017-10-13'), 13, 170), + }), ]; } \ No newline at end of file diff --git a/frontend/src/app/shared/services/usages.service.ts b/frontend/src/app/shared/services/usages.service.ts index b4eada00a..4fe3fa1fe 100644 --- a/frontend/src/app/shared/services/usages.service.ts +++ b/frontend/src/app/shared/services/usages.service.ts @@ -9,49 +9,8 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { ApiUrlConfig, DateTime, pretifyError } from '@app/framework'; - -export class CallsUsageDto { - constructor( - public readonly allowedBytes: number, - public readonly allowedCalls: number, - public readonly blockingCalls: number, - public readonly totalBytes: number, - public readonly totalCalls: number, - public readonly monthBytes: number, - public readonly monthCalls: number, - public readonly averageElapsedMs: number, - public readonly details: { [category: string]: ReadonlyArray }, - ) { - } -} - -export class CallsUsagePerDateDto { - constructor( - public readonly date: DateTime, - public readonly totalBytes: number, - public readonly totalCalls: number, - public readonly averageElapsedMs: number, - ) { - } -} - -export class StorageUsagePerDateDto { - constructor( - public readonly date: DateTime, - public readonly totalCount: number, - public readonly totalSize: number, - ) { - } -} - -export class CurrentStorageDto { - constructor( - public readonly size: number, - public readonly maxAllowed: number, - ) { - } -} +import { ApiUrlConfig, pretifyError } from '@app/framework'; +import { CallsUsageDto, CurrentStorageDto, StorageUsagePerDateDto } from '../model'; @Injectable({ providedIn: 'root', @@ -78,7 +37,7 @@ export class UsagesService { return this.http.get(url).pipe( map(body => { - return parseCurrentStorage(body); + return CurrentStorageDto.fromJSON(body); }), pretifyError('i18n:usages.loadTodayStorageFailed')); } @@ -88,7 +47,7 @@ export class UsagesService { return this.http.get(url).pipe( map(body => { - return parseCurrentStorage(body); + return CurrentStorageDto.fromJSON(body); }), pretifyError('i18n:usages.loadTodayStorageFailed')); } @@ -98,7 +57,7 @@ export class UsagesService { return this.http.get(url).pipe( map(body => { - return parseCallsUsage(body); + return CallsUsageDto.fromJSON(body); }), pretifyError('i18n:usages.loadCallsFailed')); } @@ -108,7 +67,7 @@ export class UsagesService { return this.http.get(url).pipe( map(body => { - return parseCallsUsage(body); + return CallsUsageDto.fromJSON(body); }), pretifyError('i18n:usages.loadCallsFailed')); } @@ -118,7 +77,7 @@ export class UsagesService { return this.http.get(url).pipe( map(body => { - return parseStorageUser(body); + return body.map(StorageUsagePerDateDto.fromJSON); }), pretifyError('i18n:usages.loadStorageFailed')); } @@ -128,46 +87,8 @@ export class UsagesService { return this.http.get(url).pipe( map(body => { - return parseStorageUser(body); + return body.map(StorageUsagePerDateDto.fromJSON); }), pretifyError('i18n:usages.loadStorageFailed')); } -} - -function parseCurrentStorage(response: any): CurrentStorageDto { - return new CurrentStorageDto(response.size, response.maxAllowed); -} - -function parseCallsUsage(response: any) { - const details: { [category: string]: CallsUsagePerDateDto[] } = {}; - - for (const [category, value] of Object.entries(response.details)) { - details[category] = (value as any).map((item: any) => new CallsUsagePerDateDto( - DateTime.parseISO(item.date), - item.totalBytes, - item.totalCalls, - item.averageElapsedMs)); - } - - const usages = new CallsUsageDto( - response.allowedBytes, - response.allowedCalls, - response.blockingCalls, - response.totalBytes, - response.totalCalls, - response.monthBytes, - response.monthCalls, - response.averageElapsedMs, - details); - - return usages; -} - -function parseStorageUser(response: any[]) { - const usages = response.map(item => new StorageUsagePerDateDto( - DateTime.parseISO(item.date), - item.totalCount, - item.totalSize)); - - return usages; -} +} \ No newline at end of file diff --git a/frontend/src/app/shared/services/users-provider.service.spec.ts b/frontend/src/app/shared/services/users-provider.service.spec.ts index f228b48d0..3e2c6009f 100644 --- a/frontend/src/app/shared/services/users-provider.service.spec.ts +++ b/frontend/src/app/shared/services/users-provider.service.spec.ts @@ -21,7 +21,7 @@ describe('UsersProviderService', () => { }); it('should return users service if user not cached', () => { - const user = new UserDto('123', 'User1'); + const user = new UserDto({ id: '123', displayName: 'User1' } as any); usersService.setup(x => x.getUser('123')) .returns(() => of(user)).verifiable(Times.once()); @@ -38,7 +38,7 @@ describe('UsersProviderService', () => { }); it('should return provide user from cache', () => { - const user = new UserDto('123', 'User1'); + const user = new UserDto({ id: '123', displayName: 'User1' } as any); usersService.setup(x => x.getUser('123')) .returns(() => of(user)).verifiable(Times.once()); @@ -57,7 +57,7 @@ describe('UsersProviderService', () => { }); it('should return me if user is current user', () => { - const user = new UserDto('123', 'User1'); + const user = new UserDto({ id: '123', displayName: 'User1' } as any); authService.setup(x => x.user) .returns(() => new Profile({ profile: { sub: '123' } })); @@ -71,7 +71,7 @@ describe('UsersProviderService', () => { resultingUser = result; }).unsubscribe(); - expect(resultingUser!).toEqual(new UserDto('123', 'Me')); + expect(resultingUser!).toEqual(new UserDto({ id: '123', displayName: 'Me' } as any)); usersService.verifyAll(); }); @@ -89,7 +89,7 @@ describe('UsersProviderService', () => { resultingUser = result; }).unsubscribe(); - expect(resultingUser!).toEqual(new UserDto('Unknown', 'Unknown')); + expect(resultingUser!).toEqual(new UserDto({ id: 'Unknown', displayName: 'Unknown' } as any)); usersService.verifyAll(); }); diff --git a/frontend/src/app/shared/services/users-provider.service.ts b/frontend/src/app/shared/services/users-provider.service.ts index e4aa71402..229242810 100644 --- a/frontend/src/app/shared/services/users-provider.service.ts +++ b/frontend/src/app/shared/services/users-provider.service.ts @@ -7,8 +7,9 @@ import { Injectable } from '@angular/core'; import { catchError, map, Observable, of, share, shareReplay } from 'rxjs'; +import { UserDto } from '../model'; import { AuthService } from './auth.service'; -import { UserDto, UsersService } from './users.service'; +import { UsersService } from './users.service'; @Injectable({ providedIn: 'root', @@ -29,7 +30,7 @@ export class UsersProviderService { result = this.usersService.getUser(id).pipe( catchError(() => { - return of(new UserDto('Unknown', 'Unknown')); + return of(new UserDto({ id: 'Unknown', displayName: 'Unknown' } as any)); }), shareReplay(1)); @@ -39,7 +40,7 @@ export class UsersProviderService { return result.pipe( map(dto => { if (me && this.authService.user && dto.id === this.authService.user.id) { - dto = new UserDto(dto.id, me); + dto = new UserDto({ id: dto.id, displayName: me } as any); } return dto; }), diff --git a/frontend/src/app/shared/services/users.service.spec.ts b/frontend/src/app/shared/services/users.service.spec.ts index 55731a9c4..8e0ee107c 100644 --- a/frontend/src/app/shared/services/users.service.spec.ts +++ b/frontend/src/app/shared/services/users.service.spec.ts @@ -40,7 +40,6 @@ describe('UsersService', () => { it('should make get request to get many users', inject([UsersService, HttpTestingController], (usersService: UsersService, httpMock: HttpTestingController) => { let users: ReadonlyArray; - usersService.getUsers().subscribe(result => { users = result; }); @@ -54,24 +53,25 @@ describe('UsersService', () => { { id: '123', displayName: 'User1', + _links: {}, }, { id: '456', displayName: 'User2', + _links: {}, }, ]); expect(users!).toEqual( [ - new UserDto('123', 'User1'), - new UserDto('456', 'User2'), + new UserDto({ id: '123', displayName: 'User1', _links: {} } as any), + new UserDto({ id: '456', displayName: 'User2', _links: {} } as any), ]); })); it('should make get request with query to get many users', inject([UsersService, HttpTestingController], (usersService: UsersService, httpMock: HttpTestingController) => { let users: ReadonlyArray; - usersService.getUsers('my-query').subscribe(result => { users = result; }); @@ -85,24 +85,25 @@ describe('UsersService', () => { { id: '123', displayName: 'User1', + _links: {}, }, { id: '456', displayName: 'User2', + _links: {}, }, ]); expect(users!).toEqual( [ - new UserDto('123', 'User1'), - new UserDto('456', 'User2'), + new UserDto({ id: '123', displayName: 'User1', _links: {} } as any), + new UserDto({ id: '456', displayName: 'User2', _links: {} } as any), ]); })); it('should make get request to get single user', inject([UsersService, HttpTestingController], (usersService: UsersService, httpMock: HttpTestingController) => { let user: UserDto; - usersService.getUser('123').subscribe(result => { user = result; }); @@ -114,13 +115,12 @@ describe('UsersService', () => { req.flush({ id: '123', displayName: 'User1' }); - expect(user!).toEqual(new UserDto('123', 'User1')); + expect(user!).toEqual(new UserDto({ id: '123', displayName: 'User1' } as any)); })); it('should make get request to get resources', inject([UsersService, HttpTestingController], (usersService: UsersService, httpMock: HttpTestingController) => { let resources: Resource; - usersService.getResources().subscribe(result => { resources = result; }); diff --git a/frontend/src/app/shared/services/users.service.ts b/frontend/src/app/shared/services/users.service.ts index 574f4ec08..d24dfbb6d 100644 --- a/frontend/src/app/shared/services/users.service.ts +++ b/frontend/src/app/shared/services/users.service.ts @@ -10,19 +10,7 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { ApiUrlConfig, pretifyError, Resource, StringHelper } from '@app/framework'; - -export class UserDto { - constructor( - public readonly id: string, - public readonly displayName: string, - ) { - } -} - -export type UpdateProfileDto = Readonly<{ - // The given answers. - answers: { [question: string]: string }; - }>; +import { IUpdateProfileDto, UpdateProfileDto, UserDto } from './../model'; @Injectable({ providedIn: 'root', @@ -34,10 +22,10 @@ export class UsersService { ) { } - public postUser(dto: UpdateProfileDto): Observable { + public postUser(dto: IUpdateProfileDto): Observable { const url = this.apiUrl.buildUrl('api/user'); - return this.http.post(url, dto); + return this.http.post(url, new UpdateProfileDto(dto)); } public getUsers(query?: string): Observable> { @@ -45,7 +33,7 @@ export class UsersService { return this.http.get(url).pipe( map(body => { - return parseUsers(body); + return body.map(UserDto.fromJSON); }), pretifyError('i18n:users.loadFailed')); } @@ -55,7 +43,7 @@ export class UsersService { return this.http.get(url).pipe( map(body => { - return parseUser(body); + return UserDto.fromJSON(body); }), pretifyError('i18n:users.loadUserFailed')); } @@ -66,15 +54,4 @@ export class UsersService { return this.http.get(url).pipe( pretifyError('i18n:users.loadUserFailed')); } -} - -function parseUsers(response: any[]) { - return response.map(parseUser); -} - -function parseUser(response: any) { - return new UserDto( - response.id, - response.displayName); -} - +} \ No newline at end of file diff --git a/frontend/src/app/shared/services/workflows.service.spec.ts b/frontend/src/app/shared/services/workflows.service.spec.ts index 8ff92bbcb..481c08167 100644 --- a/frontend/src/app/shared/services/workflows.service.spec.ts +++ b/frontend/src/app/shared/services/workflows.service.spec.ts @@ -10,10 +10,11 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { ApiUrlConfig, Resource, Version, WorkflowDto, WorkflowsDto, WorkflowsPayload, WorkflowsService } from '@app/shared/internal'; +import { ApiUrlConfig, Resource, Versioned, VersionTag, WorkflowDto, WorkflowsDto, WorkflowsService } from '@app/shared/internal'; +import { AddWorkflowDto, ResourceLinkDto, UpdateWorkflowDto, WorkflowStepDto, WorkflowTransitionDto } from '../model'; describe('WorkflowsService', () => { - const version = new Version('1'); + const version = new VersionTag('1'); beforeEach(() => { TestBed.configureTestingModule({ @@ -33,8 +34,7 @@ describe('WorkflowsService', () => { it('should make a get request to get app workflows', inject([WorkflowsService, HttpTestingController], (workflowsService: WorkflowsService, httpMock: HttpTestingController) => { - let workflows: WorkflowsDto; - + let workflows: Versioned; workflowsService.getWorkflows('my-app').subscribe(result => { workflows = result; }); @@ -51,14 +51,15 @@ describe('WorkflowsService', () => { }, }); - expect(workflows!).toEqual({ payload: createWorkflows('1', '2'), version: new Version('2') }); + expect(workflows!).toEqual({ payload: createWorkflows('1', '2'), version: new VersionTag('2') }); })); it('should make a post request to create a workflow', inject([WorkflowsService, HttpTestingController], (workflowsService: WorkflowsService, httpMock: HttpTestingController) => { - let workflows: WorkflowsDto; + const dto = new AddWorkflowDto({ name: 'New' }); - workflowsService.postWorkflow('my-app', { name: 'New' }, version).subscribe(result => { + let workflows: Versioned; + workflowsService.postWorkflow('my-app', dto, version).subscribe(result => { workflows = result; }); @@ -73,20 +74,21 @@ describe('WorkflowsService', () => { }, }); - expect(workflows!).toEqual({ payload: createWorkflows('1', '2'), version: new Version('2') }); + expect(workflows!).toEqual({ payload: createWorkflows('1', '2'), version: new VersionTag('2') }); })); it('should make a put request to update a workflow', inject([WorkflowsService, HttpTestingController], (workflowsService: WorkflowsService, httpMock: HttpTestingController) => { + const dto = new UpdateWorkflowDto({ initial: 'A', steps: {} }); + const resource: Resource = { _links: { update: { method: 'PUT', href: '/api/apps/my-app/workflows/123' }, }, }; - let workflows: WorkflowsDto; - - workflowsService.putWorkflow('my-app', resource, {}, version).subscribe(result => { + let workflows: Versioned; + workflowsService.putWorkflow('my-app', resource, dto, version).subscribe(result => { workflows = result; }); @@ -101,7 +103,7 @@ describe('WorkflowsService', () => { }, }); - expect(workflows!).toEqual({ payload: createWorkflows('1', '2'), version: new Version('2') }); + expect(workflows!).toEqual({ payload: createWorkflows('1', '2'), version: new VersionTag('2') }); })); it('should make a delete request to delete a workflow', @@ -112,8 +114,7 @@ describe('WorkflowsService', () => { }, }; - let workflows: WorkflowsDto; - + let workflows: Versioned; workflowsService.deleteWorkflow('my-app', resource, version).subscribe(result => { workflows = result; }); @@ -129,7 +130,7 @@ describe('WorkflowsService', () => { }, }); - expect(workflows!).toEqual({ payload: createWorkflows('1', '2'), version: new Version('2') }); + expect(workflows!).toEqual({ payload: createWorkflows('1', '2'), version: new VersionTag('2') }); })); function workflowsResponse(...names: string[]) { @@ -176,347 +177,45 @@ describe('WorkflowsService', () => { } }); -export function createWorkflows(...names: ReadonlyArray): WorkflowsPayload { - return { - items: names.map(createWorkflow), +export function createWorkflows(...names: ReadonlyArray) { + return new WorkflowsDto({ errors: [ 'Error1', 'Error2', ], - canCreate: true, - }; -} - -export function createWorkflow(name: string): WorkflowDto { - return new WorkflowDto( - { - update: { method: 'PUT', href: `/workflows/${name}` }, + items: names.map(createWorkflow), + _links: { + create: new ResourceLinkDto({ method: 'POST', href: '/workflows' }), }, - `id_${name}`, `name_${name}`, `${name}1`, - [ - `schema_${name}`, - ], - [ - { name: `${name}1`, color: `${name}1`, noUpdate: true, isLocked: false }, - { name: `${name}2`, color: `${name}2`, noUpdate: true, isLocked: false }, - ], - [ - { from: `${name}1`, to: `${name}2`, expression: 'Expression1', roles: ['Role1'] }, - { from: `${name}2`, to: `${name}1`, expression: 'Expression2', roles: ['Role2'] }, - ]); -} - -describe('Workflow', () => { - it('should create empty workflow', () => { - const workflow = new WorkflowDto({}, 'id'); - - expect(workflow.initial).toBeNull(); - }); - - it('should add step to workflow', () => { - const workflow = - new WorkflowDto({}, 'id') - .setStep('1', { color: '#00ff00' }); - - expect(workflow.serialize()).toEqual({ - name: null, - schemaIds: [], - steps: { - 1: { transitions: {}, color: '#00ff00' }, - }, - initial: '1', - }); - }); - - it('should override settings if step already exists', () => { - const workflow = - new WorkflowDto({}, 'id') - .setStep('1', { color: '#00ff00', noUpdate: true }) - .setStep('1', { color: 'red' }); - - expect(workflow.serialize()).toEqual({ - name: null, - schemaIds: [], - steps: { - 1: { transitions: {}, color: 'red', noUpdate: true }, - }, - initial: '1', - }); - }); - - it('should sort steps case invariant', () => { - const workflow = - new WorkflowDto({}, 'id') - .setStep('Z') - .setStep('a'); - - expect(workflow.steps).toEqual([ - { name: 'a' }, - { name: 'Z' }, - ]); - }); - - it('should return same workflow if step to remove is locked', () => { - const workflow = - new WorkflowDto({}, 'id') - .setStep('1', { color: '#00ff00', isLocked: true }); - - const updated = workflow.removeStep('1'); - - expect(updated).toBe(workflow); - }); - - it('should return same workflow if step to remove not found', () => { - const workflow = - new WorkflowDto({}, 'id') - .setStep('1'); - - const updated = workflow.removeStep('3'); - - expect(updated).toBe(workflow); - }); - - it('should remove step', () => { - const workflow = - new WorkflowDto({}, 'id') - .setStep('1', { color: '#00ff00' }) - .setStep('2', { color: '#ff0000' }) - .setStep('3', { color: '#0000ff' }) - .setTransition('1', '2', { expression: '1 === 2' }) - .setTransition('1', '3', { expression: '1 === 3' }) - .setTransition('2', '3', { expression: '2 === 3' }) - .removeStep('1'); - - expect(workflow.serialize()).toEqual({ - name: null, - schemaIds: [], - steps: { - 2: { - transitions: { - 3: { expression: '2 === 3' }, - }, - color: '#ff0000', - }, - 3: { transitions: {}, color: '#0000ff' }, - }, - initial: '2', - }); - }); - - it('should make first non-locked step the initial step if initial removed', () => { - const workflow = - new WorkflowDto({}, 'id') - .setStep('1') - .setStep('2', { isLocked: true }) - .setStep('3') - .removeStep('1'); - - expect(workflow.serialize()).toEqual({ - name: null, - schemaIds: [], - steps: { - 2: { transitions: {}, isLocked: true }, - 3: { transitions: {} }, - }, - initial: '3', - }); - }); - - it('should unset initial step if initial removed', () => { - const workflow = - new WorkflowDto({}, 'id') - .setStep('1') - .removeStep('1'); - - expect(workflow.serialize()).toEqual({ name: null, schemaIds: [], steps: {}, initial: null }); - }); - - it('should rename step', () => { - const workflow = - new WorkflowDto({}, 'id') - .setStep('1', { color: '#00ff00' }) - .setStep('2', { color: '#ff0000' }) - .setStep('3', { color: '#0000ff' }) - .setTransition('1', '2', { expression: '1 === 2' }) - .setTransition('2', '1', { expression: '2 === 1' }) - .setTransition('2', '3', { expression: '2 === 3' }) - .renameStep('1', 'a'); - - expect(workflow.serialize()).toEqual({ - name: null, - schemaIds: [], - steps: { - a: { - transitions: { - 2: { expression: '1 === 2' }, - }, - color: '#00ff00', - }, - 2: { - transitions: { - a: { expression: '2 === 1' }, - 3: { expression: '2 === 3' }, - }, - color: '#ff0000', - }, - 3: { transitions: {}, color: '#0000ff' }, - }, - initial: 'a', - }); }); +} - it('should add transitions to workflow', () => { - const workflow = - new WorkflowDto({}, 'id') - .setStep('1') - .setStep('2') - .setTransition('1', '2', { expression: '1 === 2' }) - .setTransition('2', '1', { expression: '2 === 1' }); - - expect(workflow.serialize()).toEqual({ - name: null, - schemaIds: [], - steps: { - 1: { - transitions: { - 2: { expression: '1 === 2' }, - }, +export function createWorkflow(name: string) { + return new WorkflowDto({ + id: `id_${name}`, + name: `name_${name}`, + initial: `${name}1`, + schemaIds: [`schema_${name}`], + steps: { + [`${name}1`]: new WorkflowStepDto({ + transitions: { + [`${name}2`]: new WorkflowTransitionDto({ + expression: 'Expression1', roles: ['Role1'], + }), }, - 2: { - transitions: { - 1: { expression: '2 === 1' }, - }, + color: `${name}1`, noUpdate: true, + }), + [`${name}2`]: new WorkflowStepDto({ + transitions: { + [`${name}1`]: new WorkflowTransitionDto({ + expression: 'Expression2', roles: ['Role2'], + }), }, - }, - initial: '1', - }); - }); - - it('should remove transition from workflow', () => { - const workflow = - new WorkflowDto({}, 'id') - .setStep('1') - .setStep('2') - .setTransition('1', '2', { expression: '1 === 2' }) - .setTransition('2', '1', { expression: '2 === 1' }) - .removeTransition('1', '2'); - - expect(workflow.serialize()).toEqual({ - name: null, - schemaIds: [], - steps: { - 1: { transitions: {} }, - 2: { - transitions: { - 1: { expression: '2 === 1' }, - }, - }, - }, - initial: '1', - }); - }); - - it('should override settings if transition already exists', () => { - const workflow = - new WorkflowDto({}, 'id') - .setStep('1') - .setStep('2') - .setTransition('2', '1', { expression: '2 === 1', roles: ['Role'] }) - .setTransition('2', '1', { expression: '2 !== 1' }); - - expect(workflow.serialize()).toEqual({ - name: null, - schemaIds: [], - steps: { - 1: { transitions: {} }, - 2: { - transitions: { - 1: { expression: '2 !== 1', roles: ['Role'] }, - }, - }, - }, - initial: '1', - }); - }); - - it('should return same workflow if transition to update not found by from step', () => { - const workflow = - new WorkflowDto({}, 'id') - .setStep('1') - .setStep('2') - .setTransition('1', '2'); - - const updated = workflow.setTransition('3', '2', { roles: ['Role'] }); - - expect(updated).toBe(workflow); - }); - - it('should return same workflow if transition to update not found by to step', () => { - const workflow = - new WorkflowDto({}, 'id') - .setStep('1') - .setStep('2') - .setTransition('1', '2'); - - const updated = workflow.setTransition('1', '3', { roles: ['Role'] }); - - expect(updated).toBe(workflow); - }); - - it('should return same workflow if transition to remove not', () => { - const workflow = - new WorkflowDto({}, 'id') - .setStep('1') - .setStep('2') - .setTransition('1', '2'); - - const updated = workflow.removeTransition('1', '3'); - - expect(updated).toBe(workflow); - }); - - it('should return same workflow if step to make initial is locked', () => { - const workflow = - new WorkflowDto({}, 'id') - .setStep('1') - .setStep('2', { color: '#00ff00', isLocked: true }); - - const updated = workflow.setInitial('2'); - - expect(updated).toBe(workflow); - }); - - it('should set initial step', () => { - const workflow = - new WorkflowDto({}, 'id') - .setStep('1') - .setStep('2') - .setInitial('2'); - - expect(workflow.serialize()).toEqual({ - name: null, - schemaIds: [], - steps: { - 1: { transitions: {} }, - 2: { transitions: {} }, - }, - initial: '2', - }); - }); - - it('should rename workflow', () => { - const workflow = - new WorkflowDto({}, 'id') - .changeName('name'); - - expect(workflow.serialize()).toEqual({ name: 'name', schemaIds: [], steps: {}, initial: null }); - }); - - it('should update schemaIds', () => { - const workflow = - new WorkflowDto({}, 'id') - .changeSchemaIds(['1', '2']); - - expect(workflow.serialize()).toEqual({ name: null, schemaIds: ['1', '2'], steps: {}, initial: null }); + color: `${name}2`, noUpdate: true, + }), + }, + _links: { + update: new ResourceLinkDto({ method: 'PUT', href: `/workflows/${name}` }), + }, }); -}); +} \ No newline at end of file diff --git a/frontend/src/app/shared/services/workflows.service.ts b/frontend/src/app/shared/services/workflows.service.ts index f4a6e3055..97f05a201 100644 --- a/frontend/src/app/shared/services/workflows.service.ts +++ b/frontend/src/app/shared/services/workflows.service.ts @@ -8,252 +8,8 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { ApiUrlConfig, compareStrings, hasAnyLink, HTTP, mapVersioned, pretifyError, Resource, ResourceLinks, StringHelper, Version, Versioned } from '@app/framework'; - -export class WorkflowDto { - public readonly _links: ResourceLinks; - - public readonly canDelete: boolean; - public readonly canUpdate: boolean; - - public readonly displayName: string; - - constructor(links: ResourceLinks, - public readonly id: string, - public readonly name: string | null = null, - public readonly initial: string | null = null, - public readonly schemaIds: string[] = [], - public readonly steps: WorkflowStep[] = [], - public readonly transitions: WorkflowTransition[] = [], - ) { - this._links = links; - - this.canDelete = hasAnyLink(links, 'delete'); - this.canUpdate = hasAnyLink(links, 'update'); - - this.displayName = StringHelper.firstNonEmpty(name, 'i18n:workflows.notNamed'); - } - - protected onCloned() { - this.steps.sort((a, b) => { - return compareStrings(a.name, b.name); - }); - - this.transitions.sort((a, b) => { - return compareStrings(a.to, b.to); - }); - } - - public getOpenSteps(step: WorkflowStep) { - return this.steps.filter(x => x.name !== step.name && !this.transitions.find(y => y.from === step.name && y.to === x.name)); - } - - public getTransitions(step: WorkflowStep): WorkflowTransitionView[] { - return this.transitions.filter(x => x.from === step.name).map(x => ({ step: this.getStep(x.to), ...x })); - } - - public getStep(name: string): WorkflowStep { - return this.steps.find(x => x.name === name)!; - } - - public setStep(name: string, values: Partial = {}) { - const old = this.getStep(name); - - const step = { ...old, name, ...values }; - const steps = [...this.steps.filter(s => s !== old), step]; - - if (steps.length === 1) { - return this.with({ initial: name, steps }); - } else { - return this.with({ steps }); - } - } - - public setTransition(from: string, to: string, values: Partial = {}) { - if (!this.getStep(from) || !this.getStep(to)) { - return this; - } - - const old = this.transitions.find(x => x.from === from && x.to === to); - - const transition = { ...old, from, to, ...values }; - const transitions = [...this.transitions.filter(t => t !== old), transition]; - - return this.with({ transitions }); - } - - public setInitial(initial: string) { - const found = this.getStep(initial); - - if (!found || found.isLocked) { - return this; - } - - return this.with({ initial }); - } - - public removeStep(name: string) { - const steps = this.steps.filter(s => s.name !== name || s.isLocked); - - if (steps.length === this.steps.length) { - return this; - } - - const transitions = this.transitions.filter(t => t.from !== name && t.to !== name); - - if (this.initial === name) { - const first = steps.find(x => !x.isLocked); - - return this.with({ initial: first?.name || null, steps, transitions }); - } else { - return this.with({ steps, transitions }); - } - } - - public changeSchemaIds(schemaIds: string[]) { - return this.with({ schemaIds }); - } - - public changeName(name: string) { - return this.with({ name }); - } - - public renameStep(name: string, newName: string) { - const steps = this.steps.map(step => { - if (step.name === name) { - return { ...step, name: newName }; - } - - return step; - }); - - const transitions = this.transitions.map(transition => { - if (transition.from === name || transition.to === name) { - const newTransition = { ...transition }; - - if (newTransition.from === name) { - newTransition.from = newName; - } - - if (newTransition.to === name) { - newTransition.to = newName; - } - - return newTransition; - } - - return transition; - }); - - if (this.initial === name) { - return this.with({ initial: newName, steps, transitions }); - } else { - return this.with({ steps, transitions }); - } - } - - public removeTransition(from: string, to: string) { - const transitions = this.transitions.filter(t => t.from !== from || t.to !== to); - - if (transitions.length === this.transitions.length) { - return this; - } - - return this.with({ transitions }); - } - - public serialize(): any { - const result = { steps: {} as Record, schemaIds: this.schemaIds, initial: this.initial, name: this.name }; - - for (const step of this.steps) { - const { name, ...values } = step; - - const s = { ...values, transitions: {} as Record }; - - for (const transition of this.getTransitions(step)) { - const { to, step: _, from: __, ...t } = transition; - - s.transitions[to] = t; - } - - result.steps[name] = s; - } - - return result; - } - - private with(update: Partial) { - const clone = Object.assign(Object.assign(Object.create(Object.getPrototypeOf(this)), this), update); - - clone.onCloned(); - - return clone; - } -} - -export type WorkflowsDto = Versioned; - -export type WorkflowsPayload = Readonly<{ - // The list of workflows. - items: WorkflowDto[]; - - // The validations errors. - errors: string[]; - - // True, if the user has permissions to create a new workflow. - canCreate?: boolean; -}>; - -export type WorkflowStepValues = Readonly<{ - // The color of the step. - color?: string; - - // True, if the step cannot be removed. - isLocked?: boolean; - - // True, if the content should be validated on this step. - validate?: boolean; - - // True, when the step has an update restriction. - noUpdate?: boolean; - - // The expression when updates are not allowed. - noUpdateExpression?: string; - - // The user roles which cannot update a content. - noUpdateRoles?: ReadonlyArray; -}>; - -export type WorkflowStep = Readonly<{ - // The name of the workflow. - name: string; -} & WorkflowStepValues>; - -export type WorkflowTransitionValues = Readonly<{ - // The expression when a transition is possible. - expression?: string; - - // The user roles which can transition to this step. - roles?: string[]; -}>; - -export type WorkflowTransition = Readonly<{ - // The source step name. - from: string; - - // The target step name. - to: string; -} & WorkflowTransitionValues>; - -export type WorkflowTransitionView = Readonly<{ - // The actual workflow step. - step: WorkflowStep; -} & WorkflowTransition>; - -export type CreateWorkflowDto = Readonly<{ - // The name of the workflow. - name: string; -}>; +import { ApiUrlConfig, HTTP, mapVersioned, pretifyError, Resource, Versioned, VersionOrTag } from '@app/framework'; +import { AddWorkflowDto, UpdateWorkflowDto, WorkflowsDto } from '../model'; @Injectable({ providedIn: 'root', @@ -265,75 +21,47 @@ export class WorkflowsService { ) { } - public getWorkflows(appName: string): Observable { + public getWorkflows(appName: string): Observable> { const url = this.apiUrl.buildUrl(`api/apps/${appName}/workflows`); return HTTP.getVersioned(this.http, url).pipe( mapVersioned(({ body }) => { - return parseWorkflows(body); + return WorkflowsDto.fromJSON(body); }), pretifyError('i18n:workflows.loadFailed')); } - public postWorkflow(appName: string, dto: CreateWorkflowDto, version: Version): Observable { + public postWorkflow(appName: string, dto: AddWorkflowDto, version: VersionOrTag): Observable> { const url = this.apiUrl.buildUrl(`api/apps/${appName}/workflows`); - return HTTP.postVersioned(this.http, url, dto, version).pipe( + return HTTP.postVersioned(this.http, url, dto.toJSON(), version).pipe( mapVersioned(({ body }) => { - return parseWorkflows(body); + return WorkflowsDto.fromJSON(body); }), pretifyError('i18n:workflows.createFailed')); } - public putWorkflow(appName: string, resource: Resource, dto: any, version: Version): Observable { + public putWorkflow(appName: string, resource: Resource, dto: UpdateWorkflowDto, version: VersionOrTag): Observable> { const link = resource._links['update']; const url = this.apiUrl.buildUrl(link.href); - return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe( + return HTTP.requestVersioned(this.http, link.method, url, version, dto.toJSON()).pipe( mapVersioned(({ body }) => { - return parseWorkflows(body); + return WorkflowsDto.fromJSON(body); }), pretifyError('i18n:workflows.updateFailed')); } - public deleteWorkflow(appName: string, resource: Resource, version: Version): Observable { + public deleteWorkflow(appName: string, resource: Resource, version: VersionOrTag): Observable> { const link = resource._links['delete']; const url = this.apiUrl.buildUrl(link.href); return HTTP.requestVersioned(this.http, link.method, url, version).pipe( mapVersioned(({ body }) => { - return parseWorkflows(body); + return WorkflowsDto.fromJSON(body); }), pretifyError('i18n:workflows.deleteFailed')); } -} - -function parseWorkflows(response: { items: any[]; errors: string[] } & Resource) { - const { items: list, errors, _links } = response; - const items = list.map(parseWorkflow); - - const canCreate = hasAnyLink(_links, 'create'); - - return { items, errors, canCreate }; -} - -function parseWorkflow(workflow: any) { - const { id, name, initial, schemaIds, _links } = workflow; - - const resultSteps: WorkflowStep[] = []; - const resultTransitions: WorkflowTransition[] = []; - - for (const [stepName, stepValue] of Object.entries(workflow.steps)) { - const { transitions, ...step } = stepValue as any; - - resultSteps.push({ name: stepName, isLocked: stepName === 'Published', ...step }); - - for (const [to, transition] of Object.entries(transitions)) { - resultTransitions.push({ from: stepName, to, ...transition as any }); - } - } - - return new WorkflowDto(_links, id, name, initial, schemaIds, resultSteps, resultTransitions); -} +} \ No newline at end of file diff --git a/frontend/src/app/shared/state/_test-helpers.ts b/frontend/src/app/shared/state/_test-helpers.ts index f8b7a9dda..8f98cb988 100644 --- a/frontend/src/app/shared/state/_test-helpers.ts +++ b/frontend/src/app/shared/state/_test-helpers.ts @@ -7,7 +7,7 @@ import { of } from 'rxjs'; import { Mock } from 'typemoq'; -import { AppsState, AuthService, DateTime, FieldPropertiesDto, FieldRule, NestedFieldDto, RootFieldDto, SchemaDto, SchemaPropertiesDto, TeamsState, Version } from '../'; +import { AppsState, AuthService, DateTime, FieldDto, FieldPropertiesDto, FieldRuleDto, NestedFieldDto, SchemaDto, SchemaPropertiesDto, SchemaScriptsDto, TeamsState, VersionTag } from '../'; const app = 'my-app'; const creation = DateTime.today().addDays(-2); @@ -15,8 +15,8 @@ const creator = 'me'; const modified = DateTime.now().addDays(-1); const modifier = 'now-me'; const team = 'my-team'; -const version = new Version('1'); -const newVersion = new Version('2'); +const version = new VersionTag('1'); +const newVersion = new VersionTag('2'); const appsState = Mock.ofType(); @@ -41,32 +41,39 @@ authService.setup(x => x.user) type SchemaValues = { id?: number; - fields?: ReadonlyArray; - fieldsInLists?: ReadonlyArray; - fieldsInReferences?: ReadonlyArray; - fieldRules?: ReadonlyArray; + name?: string; + fields?: FieldDto[]; + fieldsInLists?: string[]; + fieldsInReferences?: string[]; + fieldRules?: FieldRuleDto[]; properties?: SchemaPropertiesDto; }; -function createSchema({ properties, id, fields, fieldsInLists, fieldsInReferences, fieldRules }: SchemaValues = {}) { +function createSchema({ name, properties, id, fields, fieldsInLists, fieldsInReferences, fieldRules }: SchemaValues = {}) { id = id || 1; - return new SchemaDto({}, - `schema${id}`, - creation, - creator, - modified, - modifier, - new Version('1'), - `schema-name${id}`, - `schema-category${id}`, - 'Default', - true, - properties || new SchemaPropertiesDto(), - fields, - fieldsInLists || [], - fieldsInReferences || [], - fieldRules || []); + + return new SchemaDto({ + id: `${id}`, + category: `schema-category${id}`, + created: DateTime.now(), + createdBy: 'me', + fieldRules: fieldRules || [], + fieldsInLists: fieldsInLists || [], + fieldsInReferences: fieldsInReferences || [], + isPublished: true, + isSingleton: false, + lastModified: DateTime.now(), + lastModifiedBy: 'me', + name: name || `schema-name${id}`, + previewUrls: {}, + properties: properties || new SchemaPropertiesDto(), + scripts: new SchemaScriptsDto(), + type: 'Default', + version: 1, + fields: fields || [], + _links: {}, + }); } type FieldValues = { @@ -75,19 +82,37 @@ type FieldValues = { isDisabled?: boolean; isHidden?: boolean; partitioning?: string; - nested?: ReadonlyArray; + nested?: NestedFieldDto[]; }; function createField({ properties, id, partitioning, isDisabled, nested }: FieldValues) { id = id || 1; - return new RootFieldDto({}, id, `field${id}`, properties, partitioning || 'language', false, false, isDisabled, nested); + return new FieldDto({ + fieldId: id, + isDisabled: isDisabled || false, + isHidden: false, + isLocked: false, + name: `field${id}`, + partitioning: partitioning || 'language', + properties, + nested, + _links: {}, + }); } function createNestedField({ properties, id, isDisabled }: FieldValues) { id = id || 1; - return new NestedFieldDto({}, id, `nested${id}`, properties, 0, false, false, isDisabled); + return new NestedFieldDto({ + fieldId: id, + isDisabled: isDisabled || false, + isHidden: false, + isLocked: false, + name: `nested${id}`, + properties, + _links: {}, + }); } export const TestValues = { diff --git a/frontend/src/app/shared/state/apps.forms.ts b/frontend/src/app/shared/state/apps.forms.ts index 7dd57ffa0..f4148a5d6 100644 --- a/frontend/src/app/shared/state/apps.forms.ts +++ b/frontend/src/app/shared/state/apps.forms.ts @@ -9,7 +9,7 @@ import { UntypedFormControl, Validators } from '@angular/forms'; import { ExtendedFormGroup, Form, TemplatedFormArray, ValidatorsEx } from '@app/framework'; -import { AppDto, AppSettingsDto, CreateAppDto, TransferToTeamDto, UpdateAppDto, UpdateAppSettingsDto } from '../services/apps.service'; +import { AppDto, AppSettingsDto, CreateAppDto, EditorDto, PatternDto, TransferToTeamDto, UpdateAppDto, UpdateAppSettingsDto } from '../model'; export class CreateAppForm extends Form { constructor() { @@ -21,6 +21,10 @@ export class CreateAppForm extends Form { ]), })); } + + protected transformSubmit(value: any) { + return new CreateAppDto(value); + } } export class TransferAppForm extends Form { @@ -29,6 +33,10 @@ export class TransferAppForm extends Form { @@ -42,6 +50,10 @@ export class UpdateAppForm extends Form ), })); } + + protected transformSubmit(value: any) { + return new UpdateAppDto(value); + } } export class EditAppSettingsForm extends Form { @@ -77,6 +89,18 @@ export class EditAppSettingsForm extends Form & { editors: any[]; patterns: any[] }) { + const { editors, patterns, ...other } = value; + + return new UpdateAppSettingsDto({ + ...other, + editors: editors.map(x => + new EditorDto(x)), + patterns: patterns.map(x => + new PatternDto(x)), + }); + } } class PatternTemplate { diff --git a/frontend/src/app/shared/state/apps.state.spec.ts b/frontend/src/app/shared/state/apps.state.spec.ts index 9bbc504e1..77f40168f 100644 --- a/frontend/src/app/shared/state/apps.state.spec.ts +++ b/frontend/src/app/shared/state/apps.state.spec.ts @@ -7,7 +7,7 @@ import { firstValueFrom, of, throwError } from 'rxjs'; import { IMock, It, Mock, Times } from 'typemoq'; -import { AppsService, AppsState, DialogService } from '@app/shared/internal'; +import { AppsService, AppsState, CreateAppDto, DialogService, UpdateAppDto } from '@app/shared/internal'; import { createApp, createAppSettings } from '../services/apps.service.spec'; describe('AppsState', () => { @@ -104,7 +104,7 @@ describe('AppsState', () => { it('should add app to snapshot if created', () => { const updated = createApp(3, '_new'); - const request = { ...updated }; + const request = new CreateAppDto({ ...updated }); appsService.setup(x => x.postApp(request)) .returns(() => of(updated)).verifiable(); @@ -115,7 +115,7 @@ describe('AppsState', () => { }); it('should update app if updated', () => { - const request = {}; + const request = new UpdateAppDto({}); const updated = createApp(2, '_new'); @@ -200,7 +200,7 @@ describe('AppsState', () => { }); it('should update selected app if updated', () => { - const request = {}; + const request = new UpdateAppDto({}); const updated = createApp(1, '_new'); diff --git a/frontend/src/app/shared/state/apps.state.ts b/frontend/src/app/shared/state/apps.state.ts index 1c0f81a30..685913af9 100644 --- a/frontend/src/app/shared/state/apps.state.ts +++ b/frontend/src/app/shared/state/apps.state.ts @@ -9,7 +9,8 @@ import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { catchError, map, switchMap, tap } from 'rxjs/operators'; import { debug, DialogService, shareSubscribed, State, Types } from '@app/framework'; -import { AppDto, AppSettingsDto, AppsService, CreateAppDto, UpdateAppDto, UpdateAppSettingsDto } from '../services/apps.service'; +import { AppsService } from '../internal'; +import { AppDto, AppSettingsDto, CreateAppDto, TransferToTeamDto, UpdateAppDto, UpdateAppSettingsDto } from '../model'; interface Snapshot { // All apps, loaded once. @@ -141,8 +142,8 @@ export class AppsState extends State { shareSubscribed(this.dialogs, { silent: true })); } - public transfer(app: AppDto, teamId: string | null): Observable { - return this.appsService.transferApp(app.name, app, { teamId }, app.version).pipe( + public transfer(app: AppDto, request: TransferToTeamDto): Observable { + return this.appsService.transferApp(app.name, app, request, app.version).pipe( tap(updated => { this.replaceApp(updated); }), diff --git a/frontend/src/app/shared/state/asset-scripts.state.spec.ts b/frontend/src/app/shared/state/asset-scripts.state.spec.ts index da36baa3b..eb6259b01 100644 --- a/frontend/src/app/shared/state/asset-scripts.state.spec.ts +++ b/frontend/src/app/shared/state/asset-scripts.state.spec.ts @@ -6,9 +6,10 @@ */ import { of, onErrorResumeNextWith, throwError } from 'rxjs'; +import { customMatchers } from 'src/spec/matchers'; import { IMock, It, Mock, Times } from 'typemoq'; -import { DialogService, versioned } from '@app/shared/internal'; -import { AppsService, AssetScriptsPayload } from '../services/apps.service'; +import { DialogService, UpdateAssetScriptsDto, versioned } from '@app/shared/internal'; +import { AppsService } from '../services/apps.service'; import { createAssetScripts } from '../services/apps.service.spec'; import { TestValues } from './_test-helpers'; import { AssetScriptsState } from './asset-scripts.state'; @@ -27,6 +28,10 @@ describe('AssetScriptsState', () => { let appsService: IMock; let assetScriptsState: AssetScriptsState; + beforeAll(function () { + jasmine.addMatchers(customMatchers); + }); + beforeEach(() => { dialogs = Mock.ofType(); @@ -45,7 +50,7 @@ describe('AssetScriptsState', () => { assetScriptsState.load().subscribe(); - expect(assetScriptsState.snapshot.scripts).toEqual(oldScripts.scripts); + expect(assetScriptsState.snapshot.scripts).toEqualIgnoringProps({ query: oldScripts.query, queryPre: oldScripts.queryPre }); expect(assetScriptsState.snapshot.isLoaded).toBeTruthy(); expect(assetScriptsState.snapshot.isLoading).toBeFalsy(); expect(assetScriptsState.snapshot.version).toEqual(version); @@ -85,19 +90,15 @@ describe('AssetScriptsState', () => { it('should update scripts if scripts updated', () => { const updated = createAssetScripts(1, '_new'); - const request = { id: 'id3' }; + const request = new UpdateAssetScriptsDto({ query: 'id3' }); appsService.setup(x => x.putAssetScripts(app, oldScripts, request, version)) .returns(() => of(versioned(newVersion, updated))).verifiable(); assetScriptsState.update(request).subscribe(); - expectNewScripts(updated); - }); - - function expectNewScripts(updated: AssetScriptsPayload) { - expect(assetScriptsState.snapshot.scripts).toEqual(updated.scripts); + expect(assetScriptsState.snapshot.scripts).toEqualIgnoringProps({ query: updated.query, queryPre: updated.queryPre }); expect(assetScriptsState.snapshot.version).toEqual(newVersion); - } + }); }); }); diff --git a/frontend/src/app/shared/state/asset-scripts.state.ts b/frontend/src/app/shared/state/asset-scripts.state.ts index 3e8ef5c18..ab02bddd4 100644 --- a/frontend/src/app/shared/state/asset-scripts.state.ts +++ b/frontend/src/app/shared/state/asset-scripts.state.ts @@ -8,16 +8,21 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { finalize, tap } from 'rxjs/operators'; -import { debug, DialogService, LoadingState, Resource, shareSubscribed, State, Version } from '@app/framework'; -import { AppsService, AssetScripts, AssetScriptsPayload } from '../services/apps.service'; +import { debug, DialogService, LoadingState, Resource, shareSubscribed, State, VersionTag } from '@app/framework'; +import { AssetScriptsDto, UpdateAssetScriptsDto } from '../model'; +import { AppsService } from '../services/apps.service'; import { AppsState } from './apps.state'; +type ClassPropertiesOnly = { + [K in keyof T as T[K] extends Function ? never : K]: T[K]; +}; + interface Snapshot extends LoadingState { // The current scripts. - scripts: AssetScripts; + scripts: Omit, 'canUpdate' | 'version' | '_links'>; // The app version. - version: Version; + version: VersionTag; // Indicates if the user can update the scripts. canUpdate?: boolean; @@ -55,7 +60,7 @@ export class AssetScriptsState extends State { private readonly appsService: AppsService, private readonly dialogs: DialogService, ) { - super({ scripts: {}, resource: { _links: {} }, version: Version.EMPTY }); + super({ scripts: {}, resource: { _links: {} }, version: VersionTag.EMPTY }); debug(this, 'assetScripts'); } @@ -85,7 +90,7 @@ export class AssetScriptsState extends State { shareSubscribed(this.dialogs)); } - public update(request: AssetScripts): Observable { + public update(request: UpdateAssetScriptsDto): Observable { return this.appsService.putAssetScripts(this.appName, this.snapshot.resource, request, this.version).pipe( tap(({ version, payload }) => { this.replaceAssetScripts(payload, version); @@ -93,8 +98,8 @@ export class AssetScriptsState extends State { shareSubscribed(this.dialogs)); } - private replaceAssetScripts(payload: AssetScriptsPayload, version: Version) { - const { canUpdate, scripts } = payload; + private replaceAssetScripts(payload: AssetScriptsDto, version: VersionTag) { + const { canUpdate, _links: _, version: __, ...scripts } = payload.toJSON(); this.next({ canUpdate, diff --git a/frontend/src/app/shared/state/asset-uploader.state.ts b/frontend/src/app/shared/state/asset-uploader.state.ts index 5e85f3d8b..bc8497190 100644 --- a/frontend/src/app/shared/state/asset-uploader.state.ts +++ b/frontend/src/app/shared/state/asset-uploader.state.ts @@ -8,7 +8,8 @@ import { Injectable } from '@angular/core'; import { Observable, shareReplay, Subject, takeUntil } from 'rxjs'; import { debug, DialogService, HTTP, MathHelper, State, Types } from '@app/framework'; -import { AssetDto, AssetsService } from '../services/assets.service'; +import { AssetDto } from '../model'; +import { AssetsService } from '../services/assets.service'; import { AppsState } from './apps.state'; export interface Upload { diff --git a/frontend/src/app/shared/state/assets.forms.spec.ts b/frontend/src/app/shared/state/assets.forms.spec.ts index 4247fffc6..849461695 100644 --- a/frontend/src/app/shared/state/assets.forms.spec.ts +++ b/frontend/src/app/shared/state/assets.forms.spec.ts @@ -76,7 +76,7 @@ describe('AnnotateAssetForm', () => { const request = form.submit({ fileName: 'Old File.png' } as any)!; - expect(request).toEqual(asset); + expect(request.toJSON()).toEqual(asset); expect(form.form.enabled).toBeFalsy(); }); diff --git a/frontend/src/app/shared/state/assets.forms.ts b/frontend/src/app/shared/state/assets.forms.ts index d7b1a1e1e..bbdfc99b1 100644 --- a/frontend/src/app/shared/state/assets.forms.ts +++ b/frontend/src/app/shared/state/assets.forms.ts @@ -7,8 +7,8 @@ import { UntypedFormControl, Validators } from '@angular/forms'; import slugify from 'slugify'; -import { ExtendedFormGroup, Form, Mutable, TemplatedFormArray, Types } from '@app/framework'; -import { AnnotateAssetDto, AssetDto, AssetFolderDto, MoveAssetItemDto, RenameAssetFolderDto, RenameAssetTagDto } from '../services/assets.service'; +import { ExtendedFormGroup, Form, TemplatedFormArray, Types } from '@app/framework'; +import { AnnotateAssetDto, AssetDto, AssetFolderDto, MoveAssetDto, RenameAssetFolderDto, RenameTagDto, UpdateAssetScriptsDto } from '../model'; export class AnnotateAssetForm extends Form { public get metadata() { @@ -66,42 +66,44 @@ export class AnnotateAssetForm extends Form | null = super.submit(); + const result: ({ metadata?: object } & Record) | null = super.submit() as any; - if (asset && result) { - const index = asset.fileName.lastIndexOf('.'); + if (!asset || !result) { + return null; + } - if (index > 0) { - result.fileName += asset.fileName.substring(index); - } + const index = asset.fileName.lastIndexOf('.'); - if (result.fileName === asset.fileName) { - delete result.fileName; - } + if (index > 0) { + result.fileName += asset.fileName.substring(index); + } - if (result.slug === asset.slug) { - delete result.slug; - } + if (result.fileName === asset.fileName) { + delete result.fileName; + } - if (result.isProtected === asset.isProtected) { - delete result.isProtected; - } + if (result.slug === asset.slug) { + delete result.slug; + } - if (Types.equals(result.metadata, asset.metadata)) { - delete result.metadata; - } + if (result.isProtected === asset.isProtected) { + delete result.isProtected; + } - if (Types.equals(result.tags, asset.tags)) { - delete result.tags; - } + if (Types.equals(result.metadata, asset.metadata)) { + delete result.metadata; + } - if (Object.keys(result).length === 0) { - this.enable(); - return null; - } + if (Types.equals(result.tags, asset.tags)) { + delete result.tags; } - return result; + if (Object.keys(result).length === 0) { + this.enable(); + return null; + } + + return new AnnotateAssetDto(result); } public transformLoad(value: Partial) { @@ -172,7 +174,7 @@ class MetadataTemplate { } } -export class EditAssetScriptsForm extends Form { +export class EditAssetScriptsForm extends Form { constructor() { super(new ExtendedFormGroup({ query: new UntypedFormControl('', @@ -198,6 +200,10 @@ export class EditAssetScriptsForm extends Form { ), })); } + + public transformSubmit(value: any) { + return new UpdateAssetScriptsDto(value); + } } export class RenameAssetFolderForm extends Form { @@ -208,9 +214,13 @@ export class RenameAssetFolderForm extends Form { +export class RenameAssetTagForm extends Form { constructor() { super(new ExtendedFormGroup({ tagName: new UntypedFormControl('', @@ -218,9 +228,13 @@ export class RenameAssetTagForm extends Form { +export class MoveAssetForm extends Form { constructor() { super(new ExtendedFormGroup({ parentId: new UntypedFormControl('', @@ -228,4 +242,8 @@ export class MoveAssetForm extends Form { describe('Loading', () => { beforeEach(() => { assetsService.setup(x => x.getAssetFolders(app, MathHelper.EMPTY_GUID, 'PathAndItems')) - .returns(() => of({ items: [assetFolder1, assetFolder2], path: [] })).verifiable(Times.atLeastOnce()); + .returns(() => of(new AssetFoldersDto({ items: [assetFolder1, assetFolder2], total: 2, path: [], _links: {} }))).verifiable(Times.atLeastOnce()); }); it('should load assets', () => { assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, parentId: MathHelper.EMPTY_GUID, noSlowTotal: true })) - .returns(() => of({ items: [asset1, asset2], total: 200 })).verifiable(); + .returns(() => of(new AssetsDto({ items: [asset1, asset2], total: 200, _links: {} }))).verifiable(); assetsState.load().subscribe(); @@ -67,7 +67,7 @@ describe('AssetsState', () => { it('should show notification on load if reload is true', () => { assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, parentId: MathHelper.EMPTY_GUID, noSlowTotal: true })) - .returns(() => of({ items: [asset1, asset2], total: 200 })).verifiable(); + .returns(() => of(new AssetsDto({ items: [asset1, asset2], total: 200, _links: {} }))).verifiable(); assetsState.load(true).subscribe(); @@ -78,7 +78,7 @@ describe('AssetsState', () => { it('should load with total', () => { assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, parentId: MathHelper.EMPTY_GUID, noSlowTotal: false })) - .returns(() => of({ items: [asset1, asset2], total: 200 })).verifiable(); + .returns(() => of(new AssetsDto({ items: [asset1, asset2], total: 200, _links: {} }))).verifiable(); assetsState.load(true, false).subscribe(); @@ -89,10 +89,10 @@ describe('AssetsState', () => { it('should load without tags if tag untoggled', () => { assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, tags: ['tag1'], noSlowTotal: true })) - .returns(() => of({ items: [], total: 0 })).verifiable(); + .returns(() => of(new AssetsDto({ items: [], total: 0, _links: {} }))).verifiable(); assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, parentId: MathHelper.EMPTY_GUID, noSlowTotal: true })) - .returns(() => of({ items: [], total: 0 })).verifiable(); + .returns(() => of(new AssetsDto({ items: [], total: 0, _links: {} }))).verifiable(); assetsState.toggleTag('tag1').subscribe(); assetsState.toggleTag('tag1').subscribe(); @@ -102,7 +102,7 @@ describe('AssetsState', () => { it('should load without tags if tags reset', () => { assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, parentId: MathHelper.EMPTY_GUID, noSlowTotal: true })) - .returns(() => of({ items: [], total: 0 })).verifiable(); + .returns(() => of(new AssetsDto({ items: [], total: 0, _links: {} }))).verifiable(); assetsState.resetTags().subscribe(); @@ -111,7 +111,7 @@ describe('AssetsState', () => { it('should load with new pagination if paging', () => { assetsService.setup(x => x.getAssets(app, { take: 30, skip: 30, parentId: MathHelper.EMPTY_GUID, noSlowTotal: true })) - .returns(() => of({ items: [], total: 200 })).verifiable(); + .returns(() => of(new AssetsDto({ items: [], total: 200, _links: {} }))).verifiable(); assetsState.page({ page: 1, pageSize: 30 }).subscribe(); @@ -120,10 +120,10 @@ describe('AssetsState', () => { it('should skip page size if loaded before', () => { assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, parentId: MathHelper.EMPTY_GUID, noSlowTotal: true })) - .returns(() => of({ items: [asset1, asset2], total: 200 })).verifiable(); + .returns(() => of(new AssetsDto({ items: [asset1, asset2], total: 200, _links: {} }))).verifiable(); assetsService.setup(x => x.getAssets(app, { take: 30, skip: 30, parentId: MathHelper.EMPTY_GUID, noSlowTotal: true, noTotal: true })) - .returns(() => of({ items: [], total: 200 })).verifiable(); + .returns(() => of(new AssetsDto({ items: [], total: 200, _links: {} }))).verifiable(); assetsState.load().subscribe(); assetsState.page({ page: 1, pageSize: 30 }).subscribe(); @@ -135,10 +135,10 @@ describe('AssetsState', () => { describe('Navigating', () => { it('should load with parent id', () => { assetsService.setup(x => x.getAssetFolders(app, '123', 'PathAndItems')) - .returns(() => of({ items: [assetFolder1, assetFolder2], path: [] })).verifiable(); + .returns(() => of(new AssetFoldersDto({ items: [assetFolder1, assetFolder2], total: 2, path: [], _links: {} }))).verifiable(); assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, parentId: '123', noSlowTotal: true })) - .returns(() => of({ items: [], total: 200 })).verifiable(); + .returns(() => of(new AssetsDto({ items: [], total: 200, _links: {} }))).verifiable(); assetsState.navigate('123').subscribe(); @@ -149,7 +149,7 @@ describe('AssetsState', () => { describe('Searching', () => { it('should load with tags if tag toggled', () => { assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, tags: ['tag1'], noSlowTotal: true })) - .returns(() => of({ items: [], total: 0 })).verifiable(); + .returns(() => of(new AssetsDto({ items: [], total: 0, _links: {} }))).verifiable(); assetsState.toggleTag('tag1').subscribe(); @@ -158,7 +158,7 @@ describe('AssetsState', () => { it('should load with tags if tags selected', () => { assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, tags: ['tag1', 'tag2'], noSlowTotal: true })) - .returns(() => of({ items: [], total: 0 })).verifiable(); + .returns(() => of(new AssetsDto({ items: [], total: 0, _links: {} }))).verifiable(); assetsState.selectTags(['tag1', 'tag2']).subscribe(); @@ -169,7 +169,7 @@ describe('AssetsState', () => { const query = { fullText: 'my-query' }; assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, query, noSlowTotal: true })) - .returns(() => of({ items: [], total: 0 })).verifiable(); + .returns(() => of(new AssetsDto({ items: [], total: 0, _links: {} }))).verifiable(); assetsState.search(query).subscribe(); @@ -180,7 +180,7 @@ describe('AssetsState', () => { const query = { fullText: 'my-query' }; assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, query, noSlowTotal: true })) - .returns(() => of({ items: [], total: 0 })).verifiable(); + .returns(() => of(new AssetsDto({ items: [], total: 0, _links: {} }))).verifiable(); assetsState.next({ ref: '1' }); assetsState.search(query).subscribe(); @@ -193,13 +193,13 @@ describe('AssetsState', () => { describe('Updates', () => { beforeEach(() => { assetsService.setup(x => x.getAssetFolders(app, MathHelper.EMPTY_GUID, 'PathAndItems')) - .returns(() => of({ items: [assetFolder1, assetFolder2], path: [] })); + .returns(() => of(new AssetFoldersDto({ items: [assetFolder1, assetFolder2], total: 2, path: [], _links: {} }))); assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, parentId: MathHelper.EMPTY_GUID, noSlowTotal: true })) - .returns(() => of({ items: [asset1, asset2], total: 200 })).verifiable(); + .returns(() => of(new AssetsDto({ items: [asset1, asset2], total: 200, _links: {} }))).verifiable(); assetsService.setup(x => x.getAssets(app, { take: 2, skip: 0, parentId: MathHelper.EMPTY_GUID, noSlowTotal: true })) - .returns(() => of({ items: [asset1, asset2], total: 200 })); + .returns(() => of(new AssetsDto({ items: [asset1, asset2], total: 200, _links: {} }))); assetsState.load(true).subscribe(); }); @@ -267,9 +267,9 @@ describe('AssetsState', () => { }); it('should add asset folder if created', () => { - const request = { folderName: 'New Folder', parentId: MathHelper.EMPTY_GUID }; + const request = new CreateAssetFolderDto({ folderName: 'New Folder', parentId: MathHelper.EMPTY_GUID }); - assetsService.setup(x => x.postAssetFolder(app, It.isValue(request))) + assetsService.setup(x => x.postAssetFolder(app, request)) .returns(() => of(newAssetFolder)); assetsState.createAssetFolder(request.folderName); @@ -280,9 +280,9 @@ describe('AssetsState', () => { it('should add asset folder if path has changed', () => { const otherPath = createAssetFolder(3, '_new', 'otherParent'); - const request = { folderName: 'New Folder', parentId: MathHelper.EMPTY_GUID }; + const request = new CreateAssetFolderDto({ folderName: 'New Folder', parentId: MathHelper.EMPTY_GUID }); - assetsService.setup(x => x.postAssetFolder(app, It.isValue(request))) + assetsService.setup(x => x.postAssetFolder(app, request)) .returns(() => of(otherPath)); assetsState.createAssetFolder(request.folderName); @@ -293,7 +293,7 @@ describe('AssetsState', () => { it('should update asset if updated', () => { const updated = createAsset(1, ['new'], '_new'); - const request = { fileName: 'New Name' }; + const request = new AnnotateAssetDto({ fileName: 'New Name' }); assetsService.setup(x => x.putAsset(app, asset1, request, asset1.version)) .returns(() => of(updated)); @@ -307,7 +307,7 @@ describe('AssetsState', () => { it('should update asset folder if updated', () => { const updated = createAssetFolder(1, '_new'); - const request = { folderName: 'New Name' }; + const request = new RenameAssetFolderDto({ folderName: 'New Name' }); assetsService.setup(x => x.putAssetFolder(app, assetFolder1, request, assetFolder1.version)) .returns(() => of(updated)); @@ -320,7 +320,7 @@ describe('AssetsState', () => { it('should remove asset from snapshot if moved to other folder', () => { const updated = createAsset(1, ['new'], '_new'); - const request = { parentId: 'newParent' }; + const request = new MoveAssetDto({ parentId: 'newParent' }); assetsService.setup(x => x.putAssetParent(app, asset1, It.isValue(request), asset1.version)) .returns(() => of(updated)); @@ -334,7 +334,7 @@ describe('AssetsState', () => { it('should add asset to snapshot if moved to current folder', () => { const asset3 = createAsset(3, undefined, undefined, 'oldParent'); - const request = { parentId: assetsState.snapshot.parentId }; + const request = new MoveAssetDto({ parentId: assetsState.snapshot.parentId }); assetsService.setup(x => x.putAssetParent(app, asset3, It.isValue(request), asset3.version)) .returns(() => of(asset3)); @@ -355,7 +355,7 @@ describe('AssetsState', () => { }); it('should move asset back to snapshot if moving via api failed', () => { - const request = { parentId: 'newParent' }; + const request = new MoveAssetDto({ parentId: 'newParent' }); assetsService.setup(x => x.putAssetParent(app, asset1, It.isValue(request), asset1.version)) .returns(() => throwError(() => 'Service Error')); @@ -369,7 +369,7 @@ describe('AssetsState', () => { it('should remove asset folder from snapshot if moved to other folder', () => { const updated = createAssetFolder(1, '_new'); - const request = { parentId: 'newParent' }; + const request = new MoveAssetFolderDto({ parentId: 'newParent' }); assetsService.setup(x => x.putAssetFolderParent(app, assetFolder1, It.isValue(request), assetFolder1.version)) .returns(() => of(updated)); @@ -382,7 +382,7 @@ describe('AssetsState', () => { it('should add asset folder to snapshot if moved to current folder', () => { const assetFolder3 = createAssetFolder(3, undefined, 'oldParent'); - const request = { parentId: assetsState.snapshot.parentId }; + const request = new MoveAssetFolderDto({ parentId: assetsState.snapshot.parentId }); assetsService.setup(x => x.putAssetFolderParent(app, assetFolder3, It.isValue(request), assetFolder3.version)) .returns(() => of(assetFolder3)); @@ -393,7 +393,7 @@ describe('AssetsState', () => { }); it('should not do anything if moving asset folder to itself', () => { - const request = { parentId: assetFolder1.id }; + const request = new MoveAssetFolderDto({ parentId: assetFolder1.id }); assetsState.moveAssetFolder(assetFolder1, request.parentId).pipe(onErrorResumeNextWith()).subscribe(); @@ -401,7 +401,7 @@ describe('AssetsState', () => { }); it('should not do anything if moving asset folder to current parent', () => { - const request = { parentId: MathHelper.EMPTY_GUID }; + const request = new MoveAssetFolderDto({ parentId: MathHelper.EMPTY_GUID }); assetsState.moveAssetFolder(assetFolder1, request.parentId).pipe(onErrorResumeNextWith()).subscribe(); @@ -409,7 +409,7 @@ describe('AssetsState', () => { }); it('should move asset folder back to snapshot if moving via api failed', () => { - const request = { parentId: 'newParent' }; + const request = new MoveAssetFolderDto({ parentId: 'newParent' }); assetsService.setup(x => x.putAssetFolderParent(app, assetFolder1, It.isValue(request), assetFolder1.version)) .returns(() => throwError(() => 'Service Error')); @@ -469,14 +469,14 @@ describe('AssetsState', () => { }); it('should replace tags if renamed', () => { - const newTags = {}; + const request = new RenameTagDto({ tagName: 'new-name' }); - assetsService.setup(x => x.putTag(app, 'old-name', { tagName: 'new-name' })) - .returns(() => of(newTags)); + assetsService.setup(x => x.putTag(app, 'old-name', It.isValue(request))) + .returns(() => of({ 'new-name': 1 })); assetsState.renameTag('old-name', 'new-name').subscribe(); - expect(assetsState.snapshot.tagsAvailable).toBe(newTags); + expect(assetsState.snapshot.tagsAvailable).toEqual({ 'new-name': 1 }); }); }); }); diff --git a/frontend/src/app/shared/state/assets.state.ts b/frontend/src/app/shared/state/assets.state.ts index 03822b97d..255fb2100 100644 --- a/frontend/src/app/shared/state/assets.state.ts +++ b/frontend/src/app/shared/state/assets.state.ts @@ -9,7 +9,8 @@ import { Injectable } from '@angular/core'; import { EMPTY, forkJoin, Observable, of, throwError } from 'rxjs'; import { catchError, finalize, switchMap, tap } from 'rxjs/operators'; import { compareStrings, debug, DialogService, ErrorDto, getPagingInfo, ListState, MathHelper, shareSubscribed, State, Types } from '@app/framework'; -import { AnnotateAssetDto, AssetDto, AssetFolderDto, AssetFoldersDto, AssetsService, RenameAssetFolderDto } from '../services/assets.service'; +import { AnnotateAssetDto, AssetDto, AssetFolderDto, AssetFoldersDto, CreateAssetFolderDto, MoveAssetDto, MoveAssetFolderDto, RenameAssetFolderDto, RenameTagDto } from '../model'; +import { AssetsService } from '../services/assets.service'; import { Query } from '../services/query'; import { AppsState } from './apps.state'; @@ -260,7 +261,9 @@ export abstract class AssetsStateBase extends State { } public createAssetFolder(folderName: string) { - return this.assetsService.postAssetFolder(this.appName, { folderName, parentId: this.snapshot.parentId }).pipe( + const request = new CreateAssetFolderDto({ folderName, parentId: this.snapshot.parentId }); + + return this.assetsService.postAssetFolder(this.appName, request).pipe( tap(folder => { if (folder.parentId !== this.snapshot.parentId) { return; @@ -325,8 +328,9 @@ export abstract class AssetsStateBase extends State { } moveOutOrIn(this, asset, moveIn, 'Asset Moving'); + const request = new MoveAssetDto({ parentId }); - return this.assetsService.putAssetParent(this.appName, asset, { parentId }, asset.version).pipe( + return this.assetsService.putAssetParent(this.appName, asset, request, asset.version).pipe( catchError(error => { moveOutOrIn(this, asset, !moveIn, 'Asset Moving reverted.'); @@ -359,8 +363,9 @@ export abstract class AssetsStateBase extends State { } moveOutOrIn(this, folder, moveIn, 'Asset Folder Moving'); + const request = new MoveAssetFolderDto({ parentId }); - return this.assetsService.putAssetFolderParent(this.appName, folder, { parentId }, folder.version).pipe( + return this.assetsService.putAssetFolderParent(this.appName, folder, request, folder.version).pipe( catchError(error => { moveOutOrIn(this, folder, !moveIn, 'Asset Folder Moving reverted.'); @@ -415,7 +420,7 @@ export abstract class AssetsStateBase extends State { } public renameTag(name: string, tagName: string): Observable { - return this.assetsService.putTag(this.appName, name, { tagName }).pipe( + return this.assetsService.putTag(this.appName, name, new RenameTagDto({ tagName })).pipe( tap(tags => { this.next(s => { const tagsAvailable = tags; @@ -511,7 +516,7 @@ function updateTags(snapshot: Snapshot, newAsset?: AssetDto, oldAsset?: AssetDto const tagsAvailable = { ...snapshot.tagsAvailable }; const tagsSelected = { ...snapshot.tagsSelected }; - if (oldAsset) { + if (oldAsset?.tags) { for (const tag of oldAsset.tags) { if (tagsAvailable[tag] === 1) { delete tagsAvailable[tag]; @@ -522,7 +527,7 @@ function updateTags(snapshot: Snapshot, newAsset?: AssetDto, oldAsset?: AssetDto } } - if (newAsset) { + if (newAsset?.tags) { for (const tag of newAsset.tags) { if (tagsAvailable[tag]) { tagsAvailable[tag]++; diff --git a/frontend/src/app/shared/state/backups.forms.ts b/frontend/src/app/shared/state/backups.forms.ts index f590f1231..531b67198 100644 --- a/frontend/src/app/shared/state/backups.forms.ts +++ b/frontend/src/app/shared/state/backups.forms.ts @@ -9,9 +9,9 @@ import { UntypedFormControl, Validators } from '@angular/forms'; import { ExtendedFormGroup, Form, hasNoValue$, ValidatorsEx } from '@app/framework'; -import { StartRestoreDto } from '../services/jobs.service'; +import { RestoreRequestDto } from '../model'; -export class RestoreForm extends Form { +export class RestoreForm extends Form { public get url() { return this.form.controls['url']; } @@ -31,4 +31,8 @@ export class RestoreForm extends Form { }), ); } + + protected transformSubmit(value: any) { + return new RestoreRequestDto(value); + } } diff --git a/frontend/src/app/shared/state/clients.forms.ts b/frontend/src/app/shared/state/clients.forms.ts index 5bdab85b5..5f83d8f98 100644 --- a/frontend/src/app/shared/state/clients.forms.ts +++ b/frontend/src/app/shared/state/clients.forms.ts @@ -9,7 +9,7 @@ import { UntypedFormControl, Validators } from '@angular/forms'; import { ExtendedFormGroup, Form, hasNoValue$, ValidatorsEx } from '@app/framework'; -import { ClientDto, CreateClientDto, UpdateClientDto } from '../services/clients.service'; +import { ClientDto, CreateClientDto, UpdateClientDto } from '../model'; export class RenameClientForm extends Form { constructor() { @@ -19,6 +19,10 @@ export class RenameClientForm extends Form { @@ -36,4 +40,8 @@ export class AddClientForm extends Form { ]), })); } + + protected transformSubmit(value: any) { + return new CreateClientDto(value); + } } diff --git a/frontend/src/app/shared/state/clients.state.spec.ts b/frontend/src/app/shared/state/clients.state.spec.ts index d4196e4fd..d08ada3f1 100644 --- a/frontend/src/app/shared/state/clients.state.spec.ts +++ b/frontend/src/app/shared/state/clients.state.spec.ts @@ -7,7 +7,7 @@ import { of, onErrorResumeNextWith, throwError } from 'rxjs'; import { IMock, It, Mock, Times } from 'typemoq'; -import { ClientsPayload, ClientsService, ClientsState, DialogService, versioned } from '@app/shared/internal'; +import { ClientsDto, ClientsService, ClientsState, CreateClientDto, DialogService, UpdateClientDto, versioned } from '@app/shared/internal'; import { createClients } from '../services/clients.service.spec'; import { TestValues } from './_test-helpers'; @@ -83,7 +83,7 @@ describe('ClientsState', () => { it('should update clients if client added', () => { const updated = createClients(1, 2, 3); - const request = { id: 'id3' }; + const request = new CreateClientDto({ id: 'id3' }); clientsService.setup(x => x.postClient(app, request, version)) .returns(() => of(versioned(newVersion, updated))).verifiable(); @@ -96,7 +96,7 @@ describe('ClientsState', () => { it('should update clients if role updated', () => { const updated = createClients(1, 2, 3); - const request = { role: 'Owner' }; + const request = new UpdateClientDto({ role: 'Owner' }); clientsService.setup(x => x.putClient(app, oldClients.items[0], request, version)) .returns(() => of(versioned(newVersion, updated))).verifiable(); @@ -109,7 +109,7 @@ describe('ClientsState', () => { it('should update clients if name updated', () => { const updated = createClients(1, 2, 3); - const request = { name: 'NewName' }; + const request = new UpdateClientDto({ name: 'NewName' }); clientsService.setup(x => x.putClient(app, oldClients.items[0], request, version)) .returns(() => of(versioned(newVersion, updated))).verifiable(); @@ -130,7 +130,7 @@ describe('ClientsState', () => { expectNewClients(updated); }); - function expectNewClients(updated: ClientsPayload) { + function expectNewClients(updated: ClientsDto) { expect(clientsState.snapshot.clients).toEqual(updated.items); expect(clientsState.snapshot.version).toEqual(newVersion); } diff --git a/frontend/src/app/shared/state/clients.state.ts b/frontend/src/app/shared/state/clients.state.ts index 4809bdb1c..49d320c62 100644 --- a/frontend/src/app/shared/state/clients.state.ts +++ b/frontend/src/app/shared/state/clients.state.ts @@ -8,8 +8,8 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { finalize, tap } from 'rxjs/operators'; -import { debug, DialogService, LoadingState, shareSubscribed, State, Version } from '@app/framework'; -import { ClientDto, ClientsPayload, ClientsService, CreateClientDto, UpdateClientDto } from '../services/clients.service'; +import { debug, DialogService, LoadingState, shareSubscribed, State, VersionTag } from '@app/framework'; +import { ClientDto, ClientsDto, ClientsService, CreateClientDto, UpdateClientDto } from '../internal'; import { AppsState } from './apps.state'; interface Snapshot extends LoadingState { @@ -17,7 +17,7 @@ interface Snapshot extends LoadingState { clients: ReadonlyArray; // The app version. - version: Version; + version: VersionTag; // Indicates if the user can create new clients. canCreate?: boolean; @@ -52,7 +52,7 @@ export class ClientsState extends State { private readonly clientsService: ClientsService, private readonly dialogs: DialogService, ) { - super({ clients: [], version: Version.EMPTY }); + super({ clients: [], version: VersionTag.EMPTY }); debug(this, 'clients'); } @@ -106,7 +106,7 @@ export class ClientsState extends State { shareSubscribed(this.dialogs)); } - private replaceClients(payload: ClientsPayload, version: Version) { + private replaceClients(payload: ClientsDto, version: VersionTag) { const { canCreate, items: clients } = payload; this.next({ diff --git a/frontend/src/app/shared/state/contents.form-rules.ts b/frontend/src/app/shared/state/contents.form-rules.ts index e67999066..f47ba485a 100644 --- a/frontend/src/app/shared/state/contents.form-rules.ts +++ b/frontend/src/app/shared/state/contents.form-rules.ts @@ -9,7 +9,7 @@ /* eslint-disable no-useless-return */ import { Types } from '@app/framework'; -import { FieldRule, SchemaDto } from '../services/schemas.service'; +import { FieldRuleDto, SchemaDto } from './../model'; export type RuleContext = { data: any; user?: any }; export type RuleForm = { fieldPath: string }; @@ -36,7 +36,7 @@ export class CompiledRule { } constructor( - private readonly rule: FieldRule, + private readonly rule: FieldRuleDto, private readonly useItemData: boolean, ) { try { diff --git a/frontend/src/app/shared/state/contents.forms-helpers.ts b/frontend/src/app/shared/state/contents.forms-helpers.ts index ee0f66ffc..5f0cd9099 100644 --- a/frontend/src/app/shared/state/contents.forms-helpers.ts +++ b/frontend/src/app/shared/state/contents.forms-helpers.ts @@ -11,16 +11,15 @@ import { AbstractControl, ValidatorFn } from '@angular/forms'; import { BehaviorSubject, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { isValidValue, Language } from '../internal'; -import { AppLanguageDto } from '../services/app-languages.service'; -import { FieldDto, RootFieldDto, SchemaDto } from '../services/schemas.service'; -import { fieldInvariant } from '../services/schemas.types'; +import { AppLanguageDto, FieldDto, fieldInvariant, isValidValue, Language, NestedFieldDto, SchemaDto } from '../internal'; import { CompiledRules, RuleContext, RulesProvider } from './contents.form-rules'; -export type TranslationStatus = { [language: string]: number }; +export type TranslationStatuses = { [language: string]: number }; + +export type AnyFieldDto = FieldDto | NestedFieldDto; export function contentsTranslationStatus(datas: any[], schema: SchemaDto, languages: ReadonlyArray) { - const result: TranslationStatus = {}; + const result: TranslationStatuses = {}; for (const data of datas) { const status = contentTranslationStatus(data, schema, languages); @@ -42,7 +41,7 @@ export function contentsTranslationStatus(datas: any[], schema: SchemaDto, langu } export function contentTranslationStatus(data: any, schema: SchemaDto, languages: ReadonlyArray) { - const result: TranslationStatus = {}; + const result: TranslationStatuses = {}; const localizedFields = schema.fields.filter(x => x.isLocalizable); @@ -101,7 +100,7 @@ export abstract class Hidden { export type FieldGroup = { separator?: T; fields: T[]; id: string }; -export function groupFields(fields: ReadonlyArray, keepEmpty = false): FieldGroup[] { +export function groupFields(fields: ReadonlyArray, keepEmpty = false): FieldGroup[] { const result: FieldGroup[] = []; let currentSeparator: T | undefined; @@ -166,7 +165,7 @@ export class PartitionConfig { return { key: language.iso2Code, isOptional: language.isOptional }; } - public getAll(field: RootFieldDto) { + public getAll(field: FieldDto) { return field.isLocalizable ? this.languages : this.invariant; } } @@ -184,7 +183,7 @@ export interface FormGlobals { remoteValidator?: ValidatorFn; } -export abstract class AbstractContentForm extends Hidden { +export abstract class AbstractContentForm extends Hidden { private readonly collapsed$ = new BehaviorSubject(null); private readonly ruleSet: CompiledRules; diff --git a/frontend/src/app/shared/state/contents.forms.spec.ts b/frontend/src/app/shared/state/contents.forms.spec.ts index c5492b01f..ad0c8e22c 100644 --- a/frontend/src/app/shared/state/contents.forms.spec.ts +++ b/frontend/src/app/shared/state/contents.forms.spec.ts @@ -9,8 +9,7 @@ import { AbstractControl, UntypedFormArray } from '@angular/forms'; import { MathHelper } from '@app/framework'; -import { AppLanguageDto, createProperties, EditContentForm, getContentValue, HtmlValue, LanguageDto, RootFieldDto } from '@app/shared/internal'; -import { FieldRule, SchemaDto } from '../services/schemas.service'; +import { AppLanguageDto, createProperties, EditContentForm, FieldDto, FieldRuleDto, getContentValue, HtmlValue, SchemaDto } from '@app/shared/internal'; import { TestValues } from './_test-helpers'; import { ComponentForm, FieldArrayForm } from './contents.forms'; import { contentsTranslationStatus, contentTranslationStatus, fieldTranslationStatus, PartitionConfig } from './contents.forms-helpers'; @@ -175,7 +174,15 @@ describe('TranslationStatus', () => { }); describe('GetContentValue', () => { - const language = new LanguageDto('en', 'English'); + const language = new AppLanguageDto({ + iso2Code: 'en', + englishName: 'English', + isMaster: false, + isOptional: false, + fallback: [], + _links: {}, + }); + const fieldInvariant = createField({ properties: createProperties('Number'), partitioning: 'invariant' }); const fieldLocalized = createField({ properties: createProperties('Number') }); const fieldAssets = createField({ properties: createProperties('Assets') }); @@ -451,8 +458,22 @@ describe('GetContentValue', () => { describe('ContentForm', () => { const languages = [ - new AppLanguageDto({}, 'en', 'English', true, false, []), - new AppLanguageDto({}, 'de', 'English', false, true, []), + new AppLanguageDto({ + iso2Code: 'en', + englishName: 'English', + isMaster: true, + isOptional: false, + fallback: [], + _links: {}, + }), + new AppLanguageDto({ + iso2Code: 'de', + englishName: 'Getman', + isMaster: false, + isOptional: true, + fallback: [], + _links: {}, + }), ]; const complexSchema = createSchema({ fields: [ @@ -565,9 +586,9 @@ describe('ContentForm', () => { const contentForm = createForm([ createField({ id: 1, properties: createProperties('Number'), partitioning: 'invariant' }), createField({ id: 2, properties: createProperties('Number'), partitioning: 'invariant' }), - ], [{ - field: 'field1', action: 'Require', condition: 'ctx.value < 100', - }]); + ], [ + new FieldRuleDto({ field: 'field1', action: 'Require', condition: 'ctx.value < 100' }), + ]); contentForm.setContext({ value: 50 }); @@ -584,9 +605,9 @@ describe('ContentForm', () => { const contentForm = createForm([ createField({ id: 1, properties: createProperties('Number'), partitioning: 'invariant' }), createField({ id: 2, properties: createProperties('Number'), partitioning: 'invariant' }), - ], [{ - field: 'field1', action: 'Require', condition: 'data.field2.iv < 100', - }]); + ], [ + new FieldRuleDto({ field: 'field1', action: 'Require', condition: 'data.field2.iv < 100' }), + ]); const field1 = contentForm.get('field1')!.get('iv'); const field2 = contentForm.get('field2'); @@ -610,9 +631,9 @@ describe('ContentForm', () => { const contentForm = createForm([ createField({ id: 1, properties: createProperties('Number'), partitioning: 'invariant' }), createField({ id: 2, properties: createProperties('Number'), partitioning: 'invariant' }), - ], [{ - field: 'field1', action: 'Disable', condition: 'data.field2.iv > 100', - }]); + ], [ + new FieldRuleDto({ field: 'field1', action: 'Disable', condition: 'data.field2.iv > 100' }), + ]); const field1 = contentForm.get('field1'); const field1_iv = contentForm.get('field1')!.get('iv'); @@ -643,9 +664,9 @@ describe('ContentForm', () => { const contentForm = createForm([ createField({ id: 1, properties: createProperties('Number'), partitioning: 'invariant' }), createField({ id: 2, properties: createProperties('Number'), partitioning: 'invariant' }), - ], [{ - field: 'field1', action: 'Hide', condition: 'data.field2.iv > 100', - }]); + ], [ + new FieldRuleDto({ field: 'field1', action: 'Hide', condition: 'data.field2.iv > 100' }), + ]); const field1 = contentForm.get('field1'); const field1_iv = contentForm.get('field1')!.get('iv'); @@ -683,9 +704,9 @@ describe('ContentForm', () => { ], partitioning: 'invariant', }), - ], [{ - field: 'field4.nested42', action: 'Disable', condition: 'itemData.nested41 > 100', - }]); + ], [ + new FieldRuleDto({ field: 'field4.nested42', action: 'Disable', condition: 'itemData.nested41 > 100' }), + ]); const array = contentForm.get(complexSchema.fields[3])!.get(languages[0]) as FieldArrayForm; @@ -716,9 +737,9 @@ describe('ContentForm', () => { ], partitioning: 'invariant', }), - ], [{ - field: 'field4.nested42', action: 'Hide', condition: 'itemData.nested41 > 100', - }]); + ], [ + new FieldRuleDto({ field: 'field4.nested42', action: 'Hide', condition: 'itemData.nested41 > 100' }), + ]); const array = contentForm.get(complexSchema.fields[3])!.get(languages[0]) as FieldArrayForm; @@ -749,9 +770,9 @@ describe('ContentForm', () => { ], partitioning: 'language', }), - ], [{ - field: 'field4.nested42', action: 'Hide', condition: 'itemData.nested41 > 100', - }]); + ], [ + new FieldRuleDto({ field: 'field4.nested42', action: 'Hide', condition: 'itemData.nested41 > 100' }), + ]); const array = contentForm.get(complexSchema.fields[3])!.get(languages[0]) as FieldArrayForm; @@ -778,9 +799,9 @@ describe('ContentForm', () => { fields: [ createField({ id: 1, properties: createProperties('String'), partitioning: 'invariant' }), ], - fieldRules: [{ - field: 'field1', action: 'Hide', condition: 'data.field1 > 100', - }], + fieldRules: [ + new FieldRuleDto({ field: 'field1', action: 'Hide', condition: 'data.field1 > 100' }), + ], }); const contentForm = createForm([ @@ -1250,7 +1271,7 @@ describe('ContentForm', () => { }); }); - function createForm(fields: RootFieldDto[], fieldRules: FieldRule[] = [], schemas: { [id: string]: SchemaDto } = {}) { + function createForm(fields: FieldDto[], fieldRules: FieldRuleDto[] = [], schemas: { [id: string]: SchemaDto } = {}) { return new EditContentForm(languages, createSchema({ fields, fieldRules }), schemas, {}, 0); } diff --git a/frontend/src/app/shared/state/contents.forms.ts b/frontend/src/app/shared/state/contents.forms.ts index 3a72d34c4..55244ebdf 100644 --- a/frontend/src/app/shared/state/contents.forms.ts +++ b/frontend/src/app/shared/state/contents.forms.ts @@ -10,12 +10,9 @@ import { BehaviorSubject, Observable } from 'rxjs'; import { distinctUntilChanged, map } from 'rxjs/operators'; import { debounceTimeSafe, ExtendedFormGroup, Form, FormArrayTemplate, TemplatedFormArray, Types, value$ } from '@app/framework'; import { FormGroupTemplate, TemplatedFormGroup } from '@app/framework/angular/forms/templated-form-group'; -import { AppLanguageDto } from '../services/app-languages.service'; -import { LanguageDto } from '../services/languages.service'; -import { FieldDto, RootFieldDto, SchemaDto, TableField } from '../services/schemas.service'; -import { ComponentFieldPropertiesDto, fieldInvariant } from '../services/schemas.types'; +import { AppLanguageDto, ComponentFieldPropertiesDto, FieldDto, fieldInvariant, LanguageDto, NestedFieldDto, SchemaDto, TableField } from '../model'; import { ComponentRulesProvider, RootRulesProvider, RulesProvider } from './contents.form-rules'; -import { AbstractContentForm, AbstractContentFormState, contentTranslationStatus, FieldSection, fieldTranslationStatus, FormGlobals, groupFields, PartitionConfig } from './contents.forms-helpers'; +import { AbstractContentForm, AbstractContentFormState, AnyFieldDto, contentTranslationStatus, FieldSection, fieldTranslationStatus, FormGlobals, groupFields, PartitionConfig } from './contents.forms-helpers'; import { FieldDefaultValue, FieldsValidators } from './contents.forms.visitors'; type SaveQueryFormType = { name: string; user: boolean }; @@ -34,7 +31,7 @@ export class SaveQueryForm extends Form { } export class PatchContentForm extends Form { - private readonly editableFields: ReadonlyArray; + private readonly editableFields: ReadonlyArray; constructor( private readonly listFields: ReadonlyArray, @@ -79,7 +76,7 @@ export class EditContentForm extends Form { private readonly valueChange$ = new BehaviorSubject(this.form.value); private initialData: any; - public readonly sections: ReadonlyArray>; + public readonly sections: ReadonlyArray>; public get valueChanges(): Observable { return this.valueChange$; @@ -127,7 +124,7 @@ export class EditContentForm extends Form { this.fields[field.name] = childForm; } - return new FieldSection(separator, forms); + return new FieldSection(separator, forms); }); value$(this.form).pipe(debounceTimeSafe(debounce), distinctUntilChanged(Types.equals)).subscribe(value => { @@ -137,8 +134,8 @@ export class EditContentForm extends Form { this.updateInitialData(); } - public get(field: string | RootFieldDto): FieldForm | undefined { - if (Types.is(field, RootFieldDto)) { + public get(field: string | FieldDto): FieldForm | undefined { + if (Types.is(field, FieldDto)) { return this.fields[field.name]; } else { return this.fields[field]; @@ -208,14 +205,14 @@ export class EditContentForm extends Form { } } -export class FieldForm extends AbstractContentForm { +export class FieldForm extends AbstractContentForm { private readonly partitions: { [partition: string]: FieldItemForm } = {}; private isRequired: boolean; public readonly translationStatus = value$(this.form).pipe(map(x => fieldTranslationStatus(x))); - constructor(globals: FormGlobals, field: RootFieldDto, fieldPath: string, rules: RulesProvider) { + constructor(globals: FormGlobals, field: FieldDto, fieldPath: string, rules: RulesProvider) { super(globals, field, fieldPath, FieldForm.buildForm(), false, rules); for (const { key, isOptional } of globals.partitions.getAll(field)) { @@ -233,10 +230,10 @@ export class FieldForm extends AbstractContentForm { +export class FieldValueForm extends AbstractContentForm { private isRequired = false; - constructor(globals: FormGlobals, field: FieldDto, fieldPath: string, isOptional: boolean, rules: RulesProvider, partition: string) { + constructor(globals: FormGlobals, field: FieldDto | NestedFieldDto, fieldPath: string, isOptional: boolean, rules: RulesProvider, partition: string) { super(globals, field, fieldPath, FieldValueForm.buildControl(field, isOptional, partition, globals), isOptional, rules); - this.isRequired = field.properties.isRequired && !isOptional; + this.isRequired = !!field.properties.isRequired && !isOptional; } protected updateCustomState(_context: any, _itemData: any, state: AbstractContentFormState) { @@ -310,7 +307,7 @@ export class FieldValueForm extends AbstractContentForm { +export class FieldArrayForm extends AbstractContentForm { private readonly item$ = new BehaviorSubject>([]); public get itemChanges(): Observable> { @@ -338,7 +335,7 @@ export class FieldArrayForm extends AbstractContentForm extends AbstractContentForm { - private readonly sections$ = new BehaviorSubject>>([]); +export class ObjectFormBase extends AbstractContentForm { + private readonly sections$ = new BehaviorSubject>>([]); private readonly fields$ = new BehaviorSubject({}); - public get sectionsChanges(): Observable>> { + public get sectionsChanges(): Observable>> { return this.sections$; } @@ -466,7 +463,7 @@ export class ObjectFormBase extends Abstract return this.sections$.value; } - public set internalFieldSections(value: ReadonlyArray>) { + public set internalFieldSections(value: ReadonlyArray>) { this.sections$.next(value); } @@ -482,7 +479,7 @@ export class ObjectFormBase extends Abstract this.fields$.next(value); } - constructor(globals: FormGlobals, field: TField, fieldPath: string, isOptional: boolean, rules: RulesProvider, template: ObjectTemplate, + constructor(globals: FormGlobals, field: AnyFieldDto, fieldPath: string, isOptional: boolean, rules: RulesProvider, template: ObjectTemplate, public readonly partition: string, ) { super(globals, field, fieldPath, @@ -510,7 +507,7 @@ export class ObjectFormBase extends Abstract } abstract class ObjectTemplate implements FormGroupTemplate { - private currentSchema: ReadonlyArray | undefined; + private currentSchema: ReadonlyArray | undefined; protected get model() { return this.modelProvider(); @@ -521,7 +518,7 @@ abstract class ObjectTemplate impleme ) { } - protected abstract getSchema(value: any, model: T): ReadonlyArray | undefined; + protected abstract getSchema(value: any, model: T): ReadonlyArray | undefined; public setControls(form: UntypedFormGroup, value: any) { const schema = this.getSchema(value, this.model); @@ -547,9 +544,9 @@ abstract class ObjectTemplate impleme } } - protected setControlsCore(schema: ReadonlyArray, _: any, model: T, form: UntypedFormGroup) { + protected setControlsCore(schema: ReadonlyArray, _: any, model: T, form: UntypedFormGroup) { const fieldByName: FieldMap = {}; - const fieldSections: FieldSection[] = []; + const fieldSections: FieldSection[] = []; for (const { separator, fields } of groupFields(schema)) { const forms: FieldItemForm[] = []; @@ -570,7 +567,7 @@ abstract class ObjectTemplate impleme fieldByName[field.name] = childForm; } - fieldSections.push(new FieldSection(separator, forms)); + fieldSections.push(new FieldSection(separator, forms)); } model.internalFieldByName = fieldByName; @@ -587,8 +584,8 @@ abstract class ObjectTemplate impleme } } -export class ArrayItemForm extends ObjectFormBase { - constructor(globals: FormGlobals, field: RootFieldDto, fieldPath: string, isOptional: boolean, rules: RulesProvider, partition: string) { +export class ArrayItemForm extends ObjectFormBase { + constructor(globals: FormGlobals, field: FieldDto, fieldPath: string, isOptional: boolean, rules: RulesProvider, partition: string) { super(globals, field, fieldPath, isOptional, rules, new ArrayItemTemplate(() => this), partition); @@ -598,7 +595,11 @@ export class ArrayItemForm extends ObjectFormBase { class ArrayItemTemplate extends ObjectTemplate { public getSchema() { - return this.model.field.nested; + if (Types.is(this.model.field, FieldDto)) { + return this.model.field.nested || []; + } else { + return []; + } } } @@ -621,7 +622,7 @@ export class ComponentForm extends ObjectFormBase { return this.field.properties as ComponentFieldPropertiesDto; } - constructor(globals: FormGlobals, field: FieldDto, fieldPath: string, isOptional: boolean, rules: RulesProvider, partition: string) { + constructor(globals: FormGlobals, field: AnyFieldDto, fieldPath: string, isOptional: boolean, rules: RulesProvider, partition: string) { super(globals, field, fieldPath, isOptional, new ComponentRulesProvider(fieldPath, rules, () => this.schema), new ComponentTemplate(() => this), @@ -655,7 +656,7 @@ class ComponentTemplate extends ObjectTemplate { } } -function buildForm(globals: FormGlobals, field: FieldDto, fieldPath: string, isOptional: boolean, rules: RulesProvider, partition: string) { +function buildForm(globals: FormGlobals, field: FieldDto | NestedFieldDto, fieldPath: string, isOptional: boolean, rules: RulesProvider, partition: string) { switch (field.properties.fieldType) { case 'Array': return new FieldArrayForm(globals, field, fieldPath, isOptional, rules, partition, false); diff --git a/frontend/src/app/shared/state/contents.forms.visitors.ts b/frontend/src/app/shared/state/contents.forms.visitors.ts index 2cdd83c68..00318306e 100644 --- a/frontend/src/app/shared/state/contents.forms.visitors.ts +++ b/frontend/src/app/shared/state/contents.forms.visitors.ts @@ -7,10 +7,8 @@ import { ValidatorFn, Validators } from '@angular/forms'; import { DateTime, Types, ValidatorsEx } from '@app/framework'; -import { ContentDto, ContentReferences, ContentReferencesValue } from '../services/contents.service'; -import { LanguageDto } from '../services/languages.service'; -import { FieldDto, RootFieldDto } from '../services/schemas.service'; -import { ArrayFieldPropertiesDto, AssetsFieldPropertiesDto, BooleanFieldPropertiesDto, ComponentFieldPropertiesDto, ComponentsFieldPropertiesDto, DateTimeFieldPropertiesDto, fieldInvariant, FieldPropertiesVisitor, GeolocationFieldPropertiesDto, JsonFieldPropertiesDto, NumberFieldPropertiesDto, ReferencesFieldPropertiesDto, RichTextFieldPropertiesDto, StringFieldPropertiesDto, TagsFieldPropertiesDto, UIFieldPropertiesDto } from '../services/schemas.types'; +import { AppLanguageDto, ContentDto, FieldDto, NestedFieldDto } from '../model'; +import { ArrayFieldPropertiesDto, AssetsFieldPropertiesDto, BooleanFieldPropertiesDto, ComponentFieldPropertiesDto, ComponentsFieldPropertiesDto, DateTimeFieldPropertiesDto, fieldInvariant, FieldPropertiesVisitor, GeolocationFieldPropertiesDto, JsonFieldPropertiesDto, NumberFieldPropertiesDto, ReferencesFieldPropertiesDto, RichTextFieldPropertiesDto, StringFieldPropertiesDto, TagsFieldPropertiesDto, UIFieldPropertiesDto } from '../model'; export class HtmlValue { constructor( @@ -20,10 +18,25 @@ export class HtmlValue { } } +export type ContentReferences = Readonly<{ + // The reference values by field name. + [fieldName: string ]: ContentFieldData; +}>; + +export type ContentFieldData = Readonly<{ + // The data by partition. + [partition: string]: T; +}>; + +export type ContentReferencesValue = Readonly<{ + // The references by partition. + [partition: string]: any; +}> | string; + export type FieldValue = string | HtmlValue; -export function getContentValue(content: ContentDto, language: LanguageDto, field: RootFieldDto, allowHtml = true): { value: any; formatted: FieldValue } { - function getValue(source: ContentReferences | undefined, language: LanguageDto, field: RootFieldDto, secondLevel = false) { +export function getContentValue(content: ContentDto, language: AppLanguageDto, field: FieldDto, allowHtml = true): { value: any; formatted: FieldValue } { + function getValue(source: ContentReferences | undefined, language: AppLanguageDto, field: FieldDto, secondLevel = false) { const fieldValue = source?.[field.name]; if (!fieldValue) { @@ -64,7 +77,7 @@ export class FieldFormatter implements FieldPropertiesVisitor { ) { } - public static format(field: FieldDto, value: any, referenceValue?: any, allowHtml = true) { + public static format(field: FieldDto | NestedFieldDto, value: any, referenceValue?: any, allowHtml = true) { if ((value === null || value === undefined) && !referenceValue) { return ''; } @@ -296,7 +309,7 @@ export class FieldsValidators implements FieldPropertiesVisitor { ) { } - public static get(field: FieldDto, partitionKey: string, now?: DateTime) { + public static get(field: FieldDto | NestedFieldDto, partitionKey: string, now?: DateTime) { return field.properties.accept(new FieldDefaultValue(partitionKey, now)); } diff --git a/frontend/src/app/shared/state/contents.state.ts b/frontend/src/app/shared/state/contents.state.ts index 09dbdaa26..5de4efcf8 100644 --- a/frontend/src/app/shared/state/contents.state.ts +++ b/frontend/src/app/shared/state/contents.state.ts @@ -8,8 +8,9 @@ import { Injectable } from '@angular/core'; import { EMPTY, Observable, of } from 'rxjs'; import { catchError, finalize, map, switchMap, tap } from 'rxjs/operators'; -import { debug, DialogService, ErrorDto, getPagingInfo, ListState, shareSubscribed, State, Types, Version, Versioned } from '@app/framework'; -import { BulkResultDto, BulkUpdateJobDto, ContentDto, ContentsDto, ContentsService, StatusInfo } from '../services/contents.service'; +import { DateTime, debug, DialogService, getPagingInfo, ListState, shareSubscribed, State, Types, Version, Versioned } from '@app/framework'; +import { BulkResultDto, ContentDto, ContentsDto, BulkUpdateContentsJobDto, ServerErrorDto, IBulkUpdateContentsJobDto, BulkUpdateContentsDto } from '../model'; +import { ContentsService, StatusInfo } from '../services/contents.service'; import { Query } from '../services/query'; import { AppsState } from './apps.state'; import { SavedQuery } from './queries'; @@ -194,8 +195,6 @@ export abstract class ContentsStateBase extends State { } return this.next(s => { - statuses = s.statuses || statuses; - let selectedContent = s.selectedContent; if (selectedContent) { @@ -210,7 +209,7 @@ export abstract class ContentsStateBase extends State { isLoading: false, contents, selectedContent, - statuses, + statuses: s.statuses || statuses, total: total >= 0 ? total : s.total, }; }, 'Loading Success'); @@ -240,7 +239,7 @@ export abstract class ContentsStateBase extends State { } public validate(contents: ReadonlyArray): Observable { - const job: Partial = { type: 'Validate' }; + const job: Partial = { type: 'Validate' }; return this.bulkMany(contents, false, job).pipe( tap(results => { @@ -248,7 +247,7 @@ export abstract class ContentsStateBase extends State { const validationResults = { ...s.validationResults || {} }; for (const result of results) { - validationResults[result.contentId] = !result.error; + validationResults[result.contentId!] = !result.error; } return { ...s, validationResults }; @@ -257,8 +256,8 @@ export abstract class ContentsStateBase extends State { shareSubscribed(this.dialogs, { silent: true })); } - public changeManyStatus(contents: ReadonlyArray, status: string, dueTime?: string | null): Observable { - const job: Partial = { type: 'ChangeStatus', status, dueTime }; + public changeManyStatus(contents: ReadonlyArray, status: string, dueTime?: DateTime | undefined): Observable { + const job: Partial = { type: 'ChangeStatus', status, dueTime }; return this.bulkWithRetry(contents, job, 'i18n:contents.unpublishReferrerConfirmTitle', @@ -268,7 +267,7 @@ export abstract class ContentsStateBase extends State { } public deleteMany(contents: ReadonlyArray) { - const job: Partial = { type: 'Delete' }; + const job: Partial = { type: 'Delete' }; return this.bulkWithRetry(contents, job, 'i18n:contents.deleteReferrerConfirmTitle', @@ -358,8 +357,8 @@ export abstract class ContentsStateBase extends State { })); } - private replaceContent(content: ContentDto, oldVersion?: Version, updateText?: string) { - if (!oldVersion || !oldVersion.eq(content.version)) { + private replaceContent(content: ContentDto, oldVersion?: number, updateText?: string) { + if (!oldVersion || oldVersion != content.version) { if (updateText) { this.dialogs.notifyInfo(updateText); } @@ -379,7 +378,7 @@ export abstract class ContentsStateBase extends State { return false; } - private bulkWithRetry(contents: ReadonlyArray, job: Partial, confirmTitle: string, confirmText: string, confirmKey: string): Observable> { + private bulkWithRetry(contents: ReadonlyArray, job: Partial, confirmTitle: string, confirmText: string, confirmKey: string): Observable> { return this.bulkMany(contents, true, job).pipe( switchMap(results => { const referrerFailures = results.filter(x => isReferrerError(x.error)); @@ -414,27 +413,27 @@ export abstract class ContentsStateBase extends State { if (errors.length >= contents.length) { throw error; } else { - this.dialogs.notifyError(error); + this.dialogs.notifyError(error.toError()); } } })); } - private bulkMany(contents: ReadonlyArray, checkReferrers: boolean, job: Partial): Observable> { - const update = { + private bulkMany(contents: ReadonlyArray, checkReferrers: boolean, job: Partial): Observable> { + const update = new BulkUpdateContentsDto({ // This is set to true by default, so we turn it off here. optimizeValidation: false, - dotNotValidate: false, + doNotValidate: false, doNotScript: false, - jobs: contents.map(x => ({ + jobs: contents.map(x => new BulkUpdateContentsJobDto(({ id: x.id, schema: x.schemaName, status: undefined, - expectedVersion: parseInt(x.version.value, 10), + expectedVersion: x.version, ...job, - })), + }))), checkReferrers, - }; + }); return this.contentsService.bulkUpdate(this.appName, this.schemaName, update as any); } @@ -442,7 +441,7 @@ export abstract class ContentsStateBase extends State { public abstract get schemaName(): string; } -function isReferrerError(error?: ErrorDto) { +function isReferrerError(error?: ServerErrorDto) { return error?.errorCode === 'OBJECT_REFERENCED'; } diff --git a/frontend/src/app/shared/state/contributors.forms.ts b/frontend/src/app/shared/state/contributors.forms.ts index 4deade78b..9b766f6f8 100644 --- a/frontend/src/app/shared/state/contributors.forms.ts +++ b/frontend/src/app/shared/state/contributors.forms.ts @@ -8,8 +8,7 @@ import { UntypedFormControl, Validators } from '@angular/forms'; import { debounceTime, map, shareReplay } from 'rxjs/operators'; import { ExtendedFormGroup, Form, hasNoValue$, Types, value$ } from '@app/framework'; -import { AssignContributorDto } from '../services/shared'; -import { UserDto } from '../services/users.service'; +import { AssignContributorDto, UserDto } from '../model'; export class AssignContributorForm extends Form { public get user() { @@ -36,13 +35,11 @@ export class AssignContributorForm extends Form; - -export class ImportContributorsForm extends Form { +export class ImportContributorsForm extends Form> { public get import() { return this.form.controls['import']; } @@ -71,11 +68,10 @@ function extractEmails(value: string) { const added: { [email: string]: boolean } = {}; const emails = value.match(EMAIL_REGEX); - if (emails) { for (const match of emails) { if (!added[match]) { - result.push({ contributorId: match, role: 'Editor', invite: true }); + result.push(new AssignContributorDto({ contributorId: match, role: 'Editor', invite: true })); added[match] = true; } diff --git a/frontend/src/app/shared/state/contributors.state.spec.ts b/frontend/src/app/shared/state/contributors.state.spec.ts index 95f509a10..960ae221e 100644 --- a/frontend/src/app/shared/state/contributors.state.spec.ts +++ b/frontend/src/app/shared/state/contributors.state.spec.ts @@ -8,7 +8,7 @@ import { catchError, EMPTY, of, onErrorResumeNextWith, throwError } from 'rxjs'; import { IMock, It, Mock, Times } from 'typemoq'; import { ErrorDto } from '@app/framework'; -import { ContributorDto, ContributorsPayload, ContributorsService, ContributorsState, DialogService, versioned } from '@app/shared/internal'; +import { AssignContributorDto, ContributorDto, ContributorsDto, ContributorsService, ContributorsState, DialogService, versioned } from '@app/shared/internal'; import { createContributors } from '../services/contributors.service.spec'; import { TestValues } from './_test-helpers'; @@ -137,7 +137,7 @@ describe('ContributorsState', () => { it('should update contributors if user assigned', () => { const updated = createContributors(5, 6); - const request = { contributorId: 'mail2stehle@gmail.com', role: 'Developer' }; + const request = new AssignContributorDto({ contributorId: 'mail2stehle@gmail.com', role: 'Developer' }); contributorsService.setup(x => x.postContributor(app, request, version)) .returns(() => of(versioned(newVersion, updated))).verifiable(); @@ -148,7 +148,7 @@ describe('ContributorsState', () => { }); it('should return proper error if user to add does not exist', () => { - const request = { contributorId: 'mail2stehle@gmail.com', role: 'Developer' }; + const request = new AssignContributorDto({ contributorId: 'mail2stehle@gmail.com', role: 'Developer' }); contributorsService.setup(x => x.postContributor(app, request, version)) .returns(() => throwError(() => new ErrorDto(404, '404'))); @@ -167,7 +167,7 @@ describe('ContributorsState', () => { }); it('should return original error if not a 404', () => { - const request = { contributorId: 'mail2stehle@gmail.com', role: 'Developer' }; + const request = new AssignContributorDto({ contributorId: 'mail2stehle@gmail.com', role: 'Developer' }); contributorsService.setup(x => x.postContributor(app, request, version)) .returns(() => throwError(() => new ErrorDto(500, '500'))); @@ -196,7 +196,7 @@ describe('ContributorsState', () => { expectNewContributors(updated); }); - function expectNewContributors(updated: ContributorsPayload) { + function expectNewContributors(updated: ContributorsDto) { expect(contributorsState.snapshot.contributors).toEqual(updated.items); expect(contributorsState.snapshot.maxContributors).toBe(updated.maxContributors); expect(contributorsState.snapshot.version).toEqual(newVersion); diff --git a/frontend/src/app/shared/state/contributors.state.ts b/frontend/src/app/shared/state/contributors.state.ts index cdefd3cbd..0d08f3056 100644 --- a/frontend/src/app/shared/state/contributors.state.ts +++ b/frontend/src/app/shared/state/contributors.state.ts @@ -8,8 +8,9 @@ import { Injectable } from '@angular/core'; import { EMPTY, Observable, throwError } from 'rxjs'; import { catchError, finalize, tap } from 'rxjs/operators'; -import { debug, DialogService, ErrorDto, getPagingInfo, ListState, shareMapSubscribed, shareSubscribed, State, Types, Version } from '@app/framework'; -import { AssignContributorDto, ContributorDto, ContributorsPayload, ContributorsService } from '../services/contributors.service'; +import { debug, DialogService, ErrorDto, getPagingInfo, ListState, shareMapSubscribed, shareSubscribed, State, Types, VersionTag } from '@app/framework'; +import { ContributorDto, ContributorsDto, AssignContributorDto } from '../model'; +import { ContributorsService } from '../services/contributors.service'; import { AppsState } from './apps.state'; interface Snapshot extends ListState { @@ -20,7 +21,7 @@ interface Snapshot extends ListState { maxContributors: number; // The app version. - version: Version; + version: VersionTag; // Indicates if the user can add a contributor. canCreate?: boolean; @@ -76,7 +77,7 @@ export class ContributorsState extends State { page: 0, pageSize: 10, total: 0, - version: Version.EMPTY, + version: VersionTag.EMPTY, }); debug(this, 'contributors'); @@ -146,7 +147,7 @@ export class ContributorsState extends State { shareMapSubscribed(this.dialogs, x => x.payload.isInvited, options)); } - private replaceContributors(version: Version, { canCreate, items, maxContributors }: ContributorsPayload) { + private replaceContributors(version: VersionTag, { canCreate, items, maxContributors }: ContributorsDto) { this.next({ canCreate, contributors: items, diff --git a/frontend/src/app/shared/state/indexes.forms.ts b/frontend/src/app/shared/state/indexes.forms.ts index c78757262..f5392bd57 100644 --- a/frontend/src/app/shared/state/indexes.forms.ts +++ b/frontend/src/app/shared/state/indexes.forms.ts @@ -8,9 +8,9 @@ import { UntypedFormControl, Validators } from '@angular/forms'; import { ExtendedFormGroup, Form, TemplatedFormArray } from '@app/framework'; -import { IndexDto, IndexField } from '@app/shared/internal'; +import { CreateIndexDto, IndexFieldDto } from '@app/shared/internal'; -export class CreateIndexForm extends Form { +export class CreateIndexForm extends Form { public get controls(): ReadonlyArray { return this.form.controls as any; } @@ -18,6 +18,10 @@ export class CreateIndexForm extends Form new IndexFieldDto(x)) }); + } } class FieldTemplate { diff --git a/frontend/src/app/shared/state/indexes.state.spec.ts b/frontend/src/app/shared/state/indexes.state.spec.ts index 0577a7ee9..af3d8cc26 100644 --- a/frontend/src/app/shared/state/indexes.state.spec.ts +++ b/frontend/src/app/shared/state/indexes.state.spec.ts @@ -7,7 +7,7 @@ import { of, onErrorResumeNextWith, throwError } from 'rxjs'; import { IMock, It, Mock, Times } from 'typemoq'; -import { CreateIndexDto, DialogService, IndexesService, IndexesState, SchemasState } from '@app/shared/internal'; +import { CreateIndexDto, DialogService, IndexesService, IndexesState, IndexFieldDto, SchemasState } from '@app/shared/internal'; import { createIndex } from '../services/indexes.service.spec'; import { TestValues } from './_test-helpers'; @@ -106,7 +106,7 @@ describe('IndexesState', () => { }); it('should not add index to snapshot', () => { - const request: CreateIndexDto = { fields: [{ name: 'field1', order: 'Ascending' }] }; + const request = new CreateIndexDto({ fields: [new IndexFieldDto({ name: 'field1', order: 'Ascending' })] }); indexesService.setup(x => x.postIndex(app, schema, request)) .returns(() => of({})).verifiable(); diff --git a/frontend/src/app/shared/state/indexes.state.ts b/frontend/src/app/shared/state/indexes.state.ts index 47824a8b3..6d8e0be78 100644 --- a/frontend/src/app/shared/state/indexes.state.ts +++ b/frontend/src/app/shared/state/indexes.state.ts @@ -9,7 +9,8 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { finalize, tap } from 'rxjs/operators'; import { debug, DialogService, LoadingState, shareSubscribed, State } from '@app/framework'; -import { CreateIndexDto, IndexDto, IndexesService } from '../services/indexes.service'; +import { CreateIndexDto, IndexDto } from '../model'; +import { IndexesService } from '../services/indexes.service'; import { AppsState } from './apps.state'; import { SchemasState } from './schemas.state'; diff --git a/frontend/src/app/shared/state/jobs.state.ts b/frontend/src/app/shared/state/jobs.state.ts index fcd41b534..763924ca7 100644 --- a/frontend/src/app/shared/state/jobs.state.ts +++ b/frontend/src/app/shared/state/jobs.state.ts @@ -9,7 +9,8 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { finalize, tap } from 'rxjs/operators'; import { debug, DialogService, LoadingState, shareSubscribed, State } from '@app/framework'; -import { JobDto, JobsService } from '../services/jobs.service'; +import { JobDto } from '../model'; +import { JobsService } from '../services/jobs.service'; import { AppsState } from './apps.state'; interface Snapshot extends LoadingState { diff --git a/frontend/src/app/shared/state/languages.forms.ts b/frontend/src/app/shared/state/languages.forms.ts index 574bb5b38..e507d8822 100644 --- a/frontend/src/app/shared/state/languages.forms.ts +++ b/frontend/src/app/shared/state/languages.forms.ts @@ -7,9 +7,9 @@ import { UntypedFormControl, Validators } from '@angular/forms'; import { ExtendedFormGroup, Form, value$ } from '@app/framework'; -import { AppLanguageDto, UpdateAppLanguageDto } from '../services/app-languages.service'; +import { AddLanguageDto, AppLanguageDto, UpdateLanguageDto } from '../model'; -export class EditLanguageForm extends Form { +export class EditLanguageForm extends Form { public get isMaster() { return this.form.controls['isMaster']; } @@ -42,11 +42,13 @@ export class EditLanguageForm extends Form { +export class AddLanguageForm extends Form { constructor() { super(new ExtendedFormGroup({ language: new UntypedFormControl(null, @@ -54,4 +56,8 @@ export class AddLanguageForm extends Form { version, } = TestValues; - const languageDE = new LanguageDto('de', 'German'); - const languageEN = new LanguageDto('en', 'English'); - const languageIT = new LanguageDto('it', 'Italian'); - const languageES = new LanguageDto('es', 'Spanish'); + const languageDE = new LanguageDto({ iso2Code: 'de', englishName: 'German' }); + const languageEN = new LanguageDto({ iso2Code: 'en', englishName: 'English' }); + const languageIT = new LanguageDto({ iso2Code: 'it', englishName: 'Italian' }); + const languageES = new LanguageDto({ iso2Code: 'es', englishName: 'Spanish' }); const oldLanguages = createLanguages('en', 'de'); @@ -111,7 +111,7 @@ describe('LanguagesState', () => { it('should update languages if language updated', () => { const updated = createLanguages('de'); - const request = { isMaster: true, isOptional: false, fallback: [] }; + const request = new UpdateLanguageDto({ isMaster: true, isOptional: false, fallback: [] }); languagesService.setup(x => x.putLanguage(app, oldLanguages.items[1], request, version)) .returns(() => of(versioned(newVersion, updated))).verifiable(); @@ -132,7 +132,7 @@ describe('LanguagesState', () => { expectNewLanguages(updated); }); - function expectNewLanguages(updated: AppLanguagesPayload) { + function expectNewLanguages(updated: AppLanguagesDto) { expect(languagesState.snapshot.languages).toEqual([ { language: updated.items[0], diff --git a/frontend/src/app/shared/state/languages.state.ts b/frontend/src/app/shared/state/languages.state.ts index 0ae52cc13..bfa024905 100644 --- a/frontend/src/app/shared/state/languages.state.ts +++ b/frontend/src/app/shared/state/languages.state.ts @@ -8,9 +8,10 @@ import { Injectable } from '@angular/core'; import { forkJoin, Observable } from 'rxjs'; import { finalize, map, shareReplay, tap } from 'rxjs/operators'; -import { debug, DialogService, LoadingState, shareMapSubscribed, shareSubscribed, State, Version } from '@app/framework'; -import { AppLanguageDto, AppLanguagesPayload, AppLanguagesService, UpdateAppLanguageDto } from '../services/app-languages.service'; -import { LanguageDto, LanguagesService } from '../services/languages.service'; +import { debug, DialogService, LoadingState, shareMapSubscribed, shareSubscribed, State, VersionTag } from '@app/framework'; +import { AddLanguageDto, AppLanguageDto, AppLanguagesDto, LanguageDto, UpdateLanguageDto } from '../model'; +import { AppLanguagesService } from '../services/app-languages.service'; +import { LanguagesService } from '../services/languages.service'; import { AppsState } from './apps.state'; export interface SnapshotLanguage { @@ -35,7 +36,7 @@ interface Snapshot extends LoadingState { languages: ReadonlyArray; // The app version. - version: Version; + version: VersionTag; // Indicates if the user can add a language. canCreate?: boolean; @@ -86,7 +87,7 @@ export class LanguagesState extends State { allLanguages: [], allLanguagesNew: [], languages: [], - version: Version.EMPTY, + version: VersionTag.EMPTY, }); debug(this, 'languages'); @@ -125,7 +126,7 @@ export class LanguagesState extends State { } public add(language: string): Observable { - return this.appLanguagesService.postLanguage(this.appName, { language }, this.version).pipe( + return this.appLanguagesService.postLanguage(this.appName, new AddLanguageDto({ language }), this.version).pipe( tap(({ version, payload }) => { this.replaceLanguages(payload, version); }), @@ -140,7 +141,7 @@ export class LanguagesState extends State { shareSubscribed(this.dialogs)); } - public update(language: AppLanguageDto, request: UpdateAppLanguageDto): Observable { + public update(language: AppLanguageDto, request: UpdateLanguageDto): Observable { return this.appLanguagesService.putLanguage(this.appName, language, request, this.version).pipe( tap(({ version, payload }) => { this.replaceLanguages(payload, version); @@ -148,12 +149,11 @@ export class LanguagesState extends State { shareMapSubscribed(this.dialogs, x => x.payload)); } - private replaceLanguages(payload: AppLanguagesPayload, version: Version, allLanguages?: ReadonlyArray) { + private replaceLanguages(payload: AppLanguagesDto, version: VersionTag, allLanguages?: ReadonlyArray) { this.next(s => { allLanguages = allLanguages || s.allLanguages; const { canCreate, items: languages } = payload; - return { ...s, allLanguages, @@ -161,7 +161,7 @@ export class LanguagesState extends State { canCreate, isLoaded: true, isLoading: false, - languages: languages.map(x => this.createLanguage(x, languages)), + languages: languages.map(x => this.createLanguage(x, languages as any)), version, }; }, 'Loading Success / Updated'); diff --git a/frontend/src/app/shared/state/plans.state.spec.ts b/frontend/src/app/shared/state/plans.state.spec.ts index c1591a4f5..df2000baf 100644 --- a/frontend/src/app/shared/state/plans.state.spec.ts +++ b/frontend/src/app/shared/state/plans.state.spec.ts @@ -7,7 +7,7 @@ import { of, onErrorResumeNextWith, throwError } from 'rxjs'; import { IMock, It, Mock, Times } from 'typemoq'; -import { DialogService, PlanDto, PlanLockedReason, PlansService, PlansState, versioned } from '@app/shared/internal'; +import { DialogService, PlanChangedDto, PlanDto, PlansDto, PlansService, PlansState, versioned } from '@app/shared/internal'; import { TestValues } from './_test-helpers'; describe('PlansState', () => { @@ -19,15 +19,39 @@ describe('PlansState', () => { version, } = TestValues; - const oldPlans = { - currentPlanId: 'id1', + const oldPlans = new PlansDto({ + currentPlanId: 'free', planOwner: creator, plans: [ - new PlanDto('id1', 'name1', '100€', undefined, 'id1_yearly', '200€', undefined, 1, 1, 1, 1), - new PlanDto('id2', 'name2', '400€', undefined, 'id2_yearly', '800€', undefined, 2, 2, 2, 2), + new PlanDto({ + id: 'free', + name: 'Free', + costs: '14 €', + confirmText: 'Change for 14 € per month?', + yearlyId: 'free_yearly', + yearlyCosts: '120 €', + yearlyConfirmText: 'Change for 120 € per year?', + maxApiBytes: 128, + maxApiCalls: 1000, + maxAssetSize: 1500, + maxContributors: 2500, + }), + new PlanDto({ + id: 'professional', + name: 'Professional', + costs: '18 €', + confirmText: 'Change for 18 € per month?', + yearlyId: 'professional_yearly', + yearlyCosts: '160 €', + yearlyConfirmText: 'Change for 160 € per year?', + maxApiBytes: 512, + maxApiCalls: 4000, + maxAssetSize: 5500, + maxContributors: 6500, + }), ], - locked: 'None' as PlanLockedReason, - }; + locked: 'None', + }); let dialogs: IMock; let plansService: IMock; @@ -65,7 +89,7 @@ describe('PlansState', () => { plansService.setup(x => x.getPlans(app)) .returns(() => of(versioned(version, oldPlans))).verifiable(); - plansState.load(false, 'id2_yearly').subscribe(); + plansState.load(false, 'professional_yearly').subscribe(); expect(plansState.snapshot.plans).toEqual([ { isSelected: false, isYearlySelected: false, plan: oldPlans.plans[0] }, @@ -115,7 +139,7 @@ describe('PlansState', () => { const result = { redirectUri: 'http://url' }; plansService.setup(x => x.putPlan(app, It.isAny(), version)) - .returns(() => of(versioned(newVersion, result))); + .returns(() => of(versioned(newVersion, new PlanChangedDto(result)))); plansState.change('free').pipe(onErrorResumeNextWith()).subscribe(); @@ -127,11 +151,11 @@ describe('PlansState', () => { expect(plansState.snapshot.version).toEqual(version); }); - it('should update plans if no returning url', () => { + it('should update plans if not returning url', () => { plansService.setup(x => x.putPlan(app, It.isAny(), version)) - .returns(() => of(versioned(newVersion, { redirectUri: '' }))); + .returns(() => of(versioned(newVersion, new PlanChangedDto()))); - plansState.change('id2_yearly').pipe(onErrorResumeNextWith()).subscribe(); + plansState.change('professional_yearly').pipe(onErrorResumeNextWith()).subscribe(); expect(plansState.snapshot.plans).toEqual([ { isSelected: false, isYearlySelected: false, plan: oldPlans.plans[0] }, diff --git a/frontend/src/app/shared/state/plans.state.ts b/frontend/src/app/shared/state/plans.state.ts index a26e63a3f..170ac9961 100644 --- a/frontend/src/app/shared/state/plans.state.ts +++ b/frontend/src/app/shared/state/plans.state.ts @@ -8,8 +8,9 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { finalize, tap } from 'rxjs/operators'; -import { debug, DialogService, LoadingState, shareSubscribed, State, Version } from '@app/framework'; -import { PlanDto, PlanLockedReason, PlansService, ReferralDto } from '../services/plans.service'; +import { debug, DialogService, LoadingState, shareSubscribed, State, VersionTag } from '@app/framework'; +import { ChangePlanDto, PlanDto, PlansLockedReason, ReferralInfoDto } from '../model'; +import { PlansService } from '../services/plans.service'; import { AppsState } from './apps.state'; export interface PlanInfo { @@ -34,13 +35,13 @@ interface Snapshot extends LoadingState { portalLink?: string; // The referral info. - referral?: ReferralDto; + referral?: ReferralInfoDto; // The reason why the plan cannot be changed. - locked?: PlanLockedReason; + locked?: PlansLockedReason; // The app version. - version: Version; + version: VersionTag; } @Injectable({ @@ -83,7 +84,7 @@ export class PlansState extends State { private readonly dialogs: DialogService, private readonly plansService: PlansService, ) { - super({ plans: [], version: Version.EMPTY }); + super({ plans: [], version: VersionTag.EMPTY }); debug(this, 'plans'); } @@ -126,8 +127,8 @@ export class PlansState extends State { } public change(planId: string): Observable { - return this.plansService.putPlan(this.appName, { planId }, this.version).pipe( - tap(({ payload, version }) => { + return this.plansService.putPlan(this.appName, new ChangePlanDto({ planId }), this.version).pipe( + tap(({ version, payload }) => { if (payload.redirectUri && payload.redirectUri.length > 0) { this.window.location.href = payload.redirectUri; } else { @@ -146,7 +147,7 @@ export class PlansState extends State { } } -function createPlan(plan: PlanDto, id: string) { +function createPlan(plan: PlanDto, id?: string) { return { plan, isSelected: plan.id === id, diff --git a/frontend/src/app/shared/state/resolvers.ts b/frontend/src/app/shared/state/resolvers.ts index 3633c4f96..27185299c 100644 --- a/frontend/src/app/shared/state/resolvers.ts +++ b/frontend/src/app/shared/state/resolvers.ts @@ -8,8 +8,8 @@ import { inject, Injectable } from '@angular/core'; import { from, Observable, of, shareReplay } from 'rxjs'; import { UIOptions } from '@app/framework'; -import { AssetDto, AssetsDto, AssetsService } from '../services/assets.service'; -import { ContentDto, ContentsDto, ContentsService } from '../services/contents.service'; +import { AssetsService, ContentsService } from '../internal'; +import { AssetDto, AssetsDto, ContentDto, ContentsDto } from '../model'; import { AppsState } from './apps.state'; abstract class ResolverBase }> { diff --git a/frontend/src/app/shared/state/roles.forms.ts b/frontend/src/app/shared/state/roles.forms.ts index 82b20839c..d379ad12e 100644 --- a/frontend/src/app/shared/state/roles.forms.ts +++ b/frontend/src/app/shared/state/roles.forms.ts @@ -7,7 +7,7 @@ import { UntypedFormControl, Validators } from '@angular/forms'; import { ExtendedFormGroup, Form, hasNoValue$, TemplatedFormArray } from '@app/framework'; -import { CreateRoleDto, RoleDto, UpdateRoleDto } from '../services/roles.service'; +import { AddRoleDto, RoleDto, UpdateRoleDto } from '../model'; export class EditRoleForm extends Form { public get controls() { @@ -18,12 +18,12 @@ export class EditRoleForm extends Form) { + return value.permissions || []; } - public transformLoad(value: Partial) { - return value.permissions || []; + public transformSubmit(value: any) { + return new UpdateRoleDto({ permissions: value, properties: {} }); } } @@ -35,7 +35,7 @@ class PermissionTemplate { } } -export class AddRoleForm extends Form { +export class AddRoleForm extends Form { public get name() { return this.form.controls['name']; } @@ -49,4 +49,8 @@ export class AddRoleForm extends Form { ), })); } + + public transformSubmit(value: any) { + return new AddRoleDto(value); + } } diff --git a/frontend/src/app/shared/state/roles.state.spec.ts b/frontend/src/app/shared/state/roles.state.spec.ts index fba9a1937..6a07b164f 100644 --- a/frontend/src/app/shared/state/roles.state.spec.ts +++ b/frontend/src/app/shared/state/roles.state.spec.ts @@ -7,7 +7,7 @@ import { of, onErrorResumeNextWith, throwError } from 'rxjs'; import { IMock, It, Mock, Times } from 'typemoq'; -import { DialogService, RolesPayload, RolesService, RolesState, versioned } from '@app/shared/internal'; +import { AddRoleDto, DialogService, RolesDto, RolesService, RolesState, UpdateRoleDto, versioned } from '@app/shared/internal'; import { createRoles } from '../services/roles.service.spec'; import { TestValues } from './_test-helpers'; @@ -79,7 +79,7 @@ describe('RolesState', () => { it('should update roles if role added', () => { const updated = createRoles(4, 5); - const request = { name: 'newRole' }; + const request = new AddRoleDto({ name: 'newRole' }); rolesService.setup(x => x.postRole(app, request, version)) .returns(() => of(versioned(newVersion, updated))); @@ -92,7 +92,7 @@ describe('RolesState', () => { it('should update roles if role updated', () => { const updated = createRoles(4, 5); - const request = { permissions: ['P4', 'P5'], properties: {} }; + const request = new UpdateRoleDto({ permissions: ['P4', 'P5'], properties: {} }); rolesService.setup(x => x.putRole(app, oldRoles.items[1], request, version)) .returns(() => of(versioned(newVersion, updated))); @@ -113,7 +113,7 @@ describe('RolesState', () => { expectNewRoles(updated); }); - function expectNewRoles(updated: RolesPayload) { + function expectNewRoles(updated: RolesDto) { expect(rolesState.snapshot.roles).toEqual(updated.items); expect(rolesState.snapshot.version).toEqual(newVersion); } diff --git a/frontend/src/app/shared/state/roles.state.ts b/frontend/src/app/shared/state/roles.state.ts index fac51736b..a11464a01 100644 --- a/frontend/src/app/shared/state/roles.state.ts +++ b/frontend/src/app/shared/state/roles.state.ts @@ -8,8 +8,9 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { finalize, tap } from 'rxjs/operators'; -import { debug, DialogService, LoadingState, shareSubscribed, State, Version } from '@app/framework'; -import { CreateRoleDto, RoleDto, RolesPayload, RolesService, UpdateRoleDto } from '../services/roles.service'; +import { debug, DialogService, LoadingState, shareSubscribed, State, VersionTag } from '@app/framework'; +import { AddRoleDto, RoleDto, RolesDto, UpdateRoleDto } from '../model'; +import { RolesService } from '../services/roles.service'; import { AppsState } from './apps.state'; interface Snapshot extends LoadingState { @@ -17,7 +18,7 @@ interface Snapshot extends LoadingState { roles: ReadonlyArray; // The app version. - version: Version; + version: VersionTag; // Indicates if the user can add a role. canCreate?: boolean; @@ -58,7 +59,7 @@ export class RolesState extends State { private readonly dialogs: DialogService, private readonly rolesService: RolesService, ) { - super({ roles: [], version: Version.EMPTY }); + super({ roles: [], version: VersionTag.EMPTY }); debug(this, 'roles'); } @@ -88,7 +89,7 @@ export class RolesState extends State { shareSubscribed(this.dialogs)); } - public add(request: CreateRoleDto): Observable { + public add(request: AddRoleDto): Observable { return this.rolesService.postRole(this.appName, request, this.version).pipe( tap(({ version, payload }) => { this.replaceRoles(payload, version); @@ -112,7 +113,7 @@ export class RolesState extends State { shareSubscribed(this.dialogs)); } - private replaceRoles(payload: RolesPayload, version: Version) { + private replaceRoles(payload: RolesDto, version: VersionTag) { const { canCreate, items: roles } = payload; this.next({ diff --git a/frontend/src/app/shared/state/rule-events.state.spec.ts b/frontend/src/app/shared/state/rule-events.state.spec.ts index 013e82d84..c787bade6 100644 --- a/frontend/src/app/shared/state/rule-events.state.spec.ts +++ b/frontend/src/app/shared/state/rule-events.state.spec.ts @@ -7,7 +7,7 @@ import { of, onErrorResumeNextWith, throwError } from 'rxjs'; import { IMock, It, Mock, Times } from 'typemoq'; -import { DialogService, RuleEventsState, RulesService } from '@app/shared/internal'; +import { DialogService, RuleEventsDto, RuleEventsState, RulesService } from '@app/shared/internal'; import { createRuleEvent } from '../services/rules.service.spec'; import { TestValues } from './_test-helpers'; @@ -17,10 +17,14 @@ describe('RuleEventsState', () => { appsState, } = TestValues; - const oldRuleEvents = [ - createRuleEvent(1), - createRuleEvent(2), - ]; + const oldRuleEvents = new RuleEventsDto({ + total: 200, + items: [ + createRuleEvent(1), + createRuleEvent(2), + ], + _links: {}, + }); let dialogs: IMock; let rulesService: IMock; @@ -31,14 +35,14 @@ describe('RuleEventsState', () => { rulesService = Mock.ofType(); rulesService.setup(x => x.getEvents(app, 30, 0, undefined)) - .returns(() => of({ items: oldRuleEvents, total: 200 } as any)); + .returns(() => of(oldRuleEvents)); ruleEventsState = new RuleEventsState(appsState.object, dialogs.object, rulesService.object); ruleEventsState.load().subscribe(); }); it('should load rule events', () => { - expect(ruleEventsState.snapshot.ruleEvents).toEqual(oldRuleEvents); + expect(ruleEventsState.snapshot.ruleEvents).toEqual(oldRuleEvents.items); expect(ruleEventsState.snapshot.isLoaded).toBeTruthy(); expect(ruleEventsState.snapshot.isLoading).toBeFalsy(); expect(ruleEventsState.snapshot.total).toEqual(200); @@ -65,7 +69,7 @@ describe('RuleEventsState', () => { it('should load with new pagination if paging', () => { rulesService.setup(x => x.getEvents(app, 30, 30, undefined)) - .returns(() => of({ items: [], total: 0, _links: {} })); + .returns(() => of(new RuleEventsDto({ items: [], total: 0, _links: {} }))); ruleEventsState.page({ page: 1, pageSize: 30 }).subscribe(); @@ -77,7 +81,7 @@ describe('RuleEventsState', () => { it('should load with rule id if filtered', () => { rulesService.setup(x => x.getEvents(app, 30, 0, '12')) - .returns(() => of({ items: [], total: 200, _links: {} })); + .returns(() => of(new RuleEventsDto({ items: [], total: 200, _links: {} }))); ruleEventsState.filterByRule('12').subscribe(); @@ -88,7 +92,7 @@ describe('RuleEventsState', () => { it('should not load again if rule id has not changed', () => { rulesService.setup(x => x.getEvents(app, 30, 0, '12')) - .returns(() => of({ items: [], total: 200, _links: {} })); + .returns(() => of(new RuleEventsDto({ items: [], total: 200, _links: {} }))); ruleEventsState.filterByRule('12').subscribe(); ruleEventsState.filterByRule('12').subscribe(); @@ -99,25 +103,25 @@ describe('RuleEventsState', () => { }); it('should call service if enqueuing event', () => { - rulesService.setup(x => x.enqueueEvent(app, oldRuleEvents[0])) + rulesService.setup(x => x.enqueueEvent(app, oldRuleEvents.items[0])) .returns(() => of({})); - ruleEventsState.enqueue(oldRuleEvents[0]).subscribe(); + ruleEventsState.enqueue(oldRuleEvents.items[0]).subscribe(); expect().nothing(); - rulesService.verify(x => x.enqueueEvent(app, oldRuleEvents[0]), Times.once()); + rulesService.verify(x => x.enqueueEvent(app, oldRuleEvents.items[0]), Times.once()); }); it('should call service if cancelling event', () => { - rulesService.setup(x => x.cancelEvents(app, oldRuleEvents[0])) + rulesService.setup(x => x.cancelEvents(app, oldRuleEvents.items[0])) .returns(() => of({})); - ruleEventsState.cancel(oldRuleEvents[0]).subscribe(); + ruleEventsState.cancel(oldRuleEvents.items[0]).subscribe(); expect().nothing(); - rulesService.verify(x => x.cancelEvents(app, oldRuleEvents[0]), Times.once()); + rulesService.verify(x => x.cancelEvents(app, oldRuleEvents.items[0]), Times.once()); }); it('should call service if cancelling all events', () => { diff --git a/frontend/src/app/shared/state/rule-events.state.ts b/frontend/src/app/shared/state/rule-events.state.ts index 62f1e1f78..1513118b2 100644 --- a/frontend/src/app/shared/state/rule-events.state.ts +++ b/frontend/src/app/shared/state/rule-events.state.ts @@ -9,7 +9,8 @@ import { Injectable } from '@angular/core'; import { EMPTY, Observable } from 'rxjs'; import { finalize, tap } from 'rxjs/operators'; import { debug, DialogService, getPagingInfo, ListState, Resource, shareSubscribed, State } from '@app/framework'; -import { RuleEventDto, RulesService } from '../services/rules.service'; +import { RuleEventDto } from '../model'; +import { RulesService } from '../services/rules.service'; import { AppsState } from './apps.state'; interface Snapshot extends ListState { @@ -163,14 +164,15 @@ export class RuleEventsState extends State { } const setCancelled = (event: RuleEventDto) => - new RuleEventDto( - event._links, - event.id, - event.created, - null, - event.eventName, - event.description, - event.lastDump, - event.result, - 'Cancelled', - event.numCalls); \ No newline at end of file + new RuleEventDto({ + id: event.id, + created: event.created, + description: event.description, + eventName: event.eventName, + jobResult: 'Cancelled', + lastDump: event.lastDump, + nextAttempt: undefined, + numCalls: event.numCalls, + result: event.result, + _links: event._links, + }); \ No newline at end of file diff --git a/frontend/src/app/shared/state/rule-simulator.state.spec.ts b/frontend/src/app/shared/state/rule-simulator.state.spec.ts index 60cae88c9..c32382653 100644 --- a/frontend/src/app/shared/state/rule-simulator.state.spec.ts +++ b/frontend/src/app/shared/state/rule-simulator.state.spec.ts @@ -7,7 +7,7 @@ import { of, onErrorResumeNextWith, throwError } from 'rxjs'; import { IMock, It, Mock, Times } from 'typemoq'; -import { DialogService, RulesService } from '@app/shared/internal'; +import { DialogService, RulesService, SimulatedRuleEventsDto } from '@app/shared/internal'; import { createSimulatedRuleEvent } from '../services/rules.service.spec'; import { TestValues } from './_test-helpers'; import { RuleSimulatorState } from './rule-simulator.state'; @@ -18,10 +18,14 @@ describe('RuleSimulatorState', () => { appsState, } = TestValues; - const oldSimulatedRuleEvents = [ - createSimulatedRuleEvent(1), - createSimulatedRuleEvent(2), - ]; + const oldSimulatedRuleEvents = new SimulatedRuleEventsDto({ + total: 200, + items: [ + createSimulatedRuleEvent(1), + createSimulatedRuleEvent(2), + ], + _links: {}, + }); let dialogs: IMock; let rulesService: IMock; @@ -37,12 +41,12 @@ describe('RuleSimulatorState', () => { it('should load simulated rule events', () => { rulesService.setup(x => x.getSimulatedEvents(app, '12')) - .returns(() => of({ items: oldSimulatedRuleEvents, total: 200 })); + .returns(() => of(oldSimulatedRuleEvents)); ruleSimulatorState.selectRule('12'); ruleSimulatorState.load().subscribe(); - expect(ruleSimulatorState.snapshot.simulatedRuleEvents).toEqual(oldSimulatedRuleEvents); + expect(ruleSimulatorState.snapshot.simulatedRuleEvents).toEqual(oldSimulatedRuleEvents.items); expect(ruleSimulatorState.snapshot.isLoaded).toBeTruthy(); expect(ruleSimulatorState.snapshot.isLoading).toBeFalsy(); expect(ruleSimulatorState.snapshot.total).toEqual(200); @@ -52,12 +56,12 @@ describe('RuleSimulatorState', () => { it('should load simulated rule events by action and trigger', () => { rulesService.setup(x => x.postSimulatedEvents(app, It.isAny(), It.isAny())) - .returns(() => of({ items: oldSimulatedRuleEvents, total: 200 })); + .returns(() => of(oldSimulatedRuleEvents)); ruleSimulatorState.setRule({}, {}); ruleSimulatorState.load().subscribe(); - expect(ruleSimulatorState.snapshot.simulatedRuleEvents).toEqual(oldSimulatedRuleEvents); + expect(ruleSimulatorState.snapshot.simulatedRuleEvents).toEqual(oldSimulatedRuleEvents.items); expect(ruleSimulatorState.snapshot.isLoaded).toBeTruthy(); expect(ruleSimulatorState.snapshot.isLoading).toBeFalsy(); expect(ruleSimulatorState.snapshot.total).toEqual(200); diff --git a/frontend/src/app/shared/state/rule-simulator.state.ts b/frontend/src/app/shared/state/rule-simulator.state.ts index 4c3591458..60d3d7d34 100644 --- a/frontend/src/app/shared/state/rule-simulator.state.ts +++ b/frontend/src/app/shared/state/rule-simulator.state.ts @@ -9,7 +9,8 @@ import { Injectable } from '@angular/core'; import { EMPTY, Observable } from 'rxjs'; import { finalize, tap } from 'rxjs/operators'; import { debug, DialogService, ListState, shareSubscribed, State } from '@app/framework'; -import { RulesService, SimulatedRuleEventDto } from '../services/rules.service'; +import { SimulatedRuleEventDto } from '../model'; +import { RulesService } from '../services/rules.service'; import { AppsState } from './apps.state'; interface Snapshot extends ListState { diff --git a/frontend/src/app/shared/state/rules.forms.ts b/frontend/src/app/shared/state/rules.forms.ts index ff1bb082b..ff21c16da 100644 --- a/frontend/src/app/shared/state/rules.forms.ts +++ b/frontend/src/app/shared/state/rules.forms.ts @@ -7,9 +7,9 @@ import { AbstractControl, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms'; import { ExtendedFormGroup, Form, TemplatedFormArray, ValidatorsEx } from '@app/framework'; -import { RuleElementDto, TriggerType } from '../services/rules.service'; +import { RuleElementDto, RuleTriggerDto } from '../model'; -export class ActionForm extends Form { +export class ActionForm extends Form> { constructor(public readonly definition: RuleElementDto, public readonly actionType: string, ) { @@ -44,9 +44,9 @@ export class ActionForm extends Form { } } -export class TriggerForm extends Form { +export class TriggerForm extends Form { constructor( - public readonly triggerType: TriggerType, + public readonly triggerType: string, ) { super(TriggerForm.builForm(triggerType)); } @@ -86,10 +86,10 @@ export class TriggerForm extends Form { } } - protected transformSubmit(value: any): any { + protected transformSubmit(value: any) { value.triggerType = this.triggerType; - return value; + return RuleTriggerDto.fromJSON(value); } } diff --git a/frontend/src/app/shared/state/rules.state.spec.ts b/frontend/src/app/shared/state/rules.state.spec.ts index aa6f5f08b..d241ebe5d 100644 --- a/frontend/src/app/shared/state/rules.state.spec.ts +++ b/frontend/src/app/shared/state/rules.state.spec.ts @@ -7,8 +7,7 @@ import { firstValueFrom, of, onErrorResumeNextWith, throwError } from 'rxjs'; import { IMock, It, Mock, Times } from 'typemoq'; -import { DialogService, RulesService, versioned } from '@app/shared/internal'; -import { RuleDto } from '../services/rules.service'; +import { DialogService, DynamicCreateRuleDto, DynamicRuleDto, DynamicRulesDto, DynamicUpdateRuleDto, ManualRuleTriggerDto, RulesService, versioned } from '@app/shared/internal'; import { createRule } from '../services/rules.service.spec'; import { TestValues } from './_test-helpers'; import { RulesState } from './rules.state'; @@ -43,7 +42,7 @@ describe('RulesState', () => { describe('Loading', () => { it('should load rules', () => { rulesService.setup(x => x.getRules(app)) - .returns(() => of({ items: [rule1, rule2], runningRuleId: rule1.id })).verifiable(); + .returns(() => of(new DynamicRulesDto(({ items: [rule1, rule2], runningRuleId: rule1.id, _links: {} })))).verifiable(); rulesState.load().subscribe(); @@ -51,8 +50,7 @@ describe('RulesState', () => { expect(rulesState.snapshot.isLoading).toBeFalsy(); expect(rulesState.snapshot.rules).toEqual([rule1, rule2]); - let ruleRunning: RuleDto | undefined; - + let ruleRunning: DynamicRuleDto | undefined; rulesState.runningRule.subscribe(result => { ruleRunning = result; }); @@ -74,7 +72,7 @@ describe('RulesState', () => { it('should show notification on load if reload is true', () => { rulesService.setup(x => x.getRules(app)) - .returns(() => of({ items: [rule1, rule2] })).verifiable(); + .returns(() => of(new DynamicRulesDto(({ items: [rule1, rule2], _links: {} })))).verifiable(); rulesState.load(true).subscribe(); @@ -87,7 +85,7 @@ describe('RulesState', () => { describe('Updates', () => { beforeEach(() => { rulesService.setup(x => x.getRules(app)) - .returns(() => of({ items: [rule1, rule2] })).verifiable(); + .returns(() => of(new DynamicRulesDto(({ items: [rule1, rule2], _links: {} })))).verifiable(); rulesState.load().subscribe(); }); @@ -107,7 +105,12 @@ describe('RulesState', () => { }); it('should add rule to snapshot if created', () => { - const request = { trigger: { triggerType: 'trigger3', value: 3 }, action: { actionType: 'action3', value: 1 } }; + const request = new DynamicCreateRuleDto({ + trigger: new ManualRuleTriggerDto(), + action: { + actionType: 'action3', + }, + }); rulesService.setup(x => x.postRule(app, request)) .returns(() => of(newRule)); @@ -118,7 +121,12 @@ describe('RulesState', () => { }); it('should update rule if updated', () => { - const request = {}; + const request = new DynamicUpdateRuleDto({ + trigger: new ManualRuleTriggerDto(), + action: { + actionType: 'action3', + }, + }); const updated = createRule(1, '_new'); @@ -179,7 +187,7 @@ describe('RulesState', () => { describe('Selection', () => { beforeEach(() => { rulesService.setup(x => x.getRules(app)) - .returns(() => of({ items: [rule1, rule2] })).verifiable(Times.atLeastOnce()); + .returns(() => of(new DynamicRulesDto(({ items: [rule1, rule2], _links: {} })))).verifiable(Times.atLeastOnce()); rulesState.load().subscribe(); rulesState.select(rule2.id).subscribe(); @@ -192,7 +200,7 @@ describe('RulesState', () => { ]; rulesService.setup(x => x.getRules(app)) - .returns(() => of({ items: newRules })); + .returns(() => of(new DynamicRulesDto(({ items: newRules, _links: {} })))).verifiable(Times.exactly(2)); rulesState.load().subscribe(); @@ -200,7 +208,12 @@ describe('RulesState', () => { }); it('should update selected rule if updated', () => { - const request = {}; + const request = new DynamicUpdateRuleDto({ + trigger: new ManualRuleTriggerDto(), + action: { + actionType: 'action3', + }, + }); const updated = createRule(2, '_new'); diff --git a/frontend/src/app/shared/state/rules.state.ts b/frontend/src/app/shared/state/rules.state.ts index 33f20ce65..d956ac52b 100644 --- a/frontend/src/app/shared/state/rules.state.ts +++ b/frontend/src/app/shared/state/rules.state.ts @@ -9,15 +9,16 @@ import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { finalize, map, tap } from 'rxjs/operators'; import { debug, DialogService, LoadingState, shareSubscribed, State } from '@app/framework'; -import { RuleDto, RulesService, UpsertRuleDto } from '../services/rules.service'; +import { DynamicRuleDto, DynamicCreateRuleDto, DynamicUpdateRuleDto } from '../model'; +import { RulesService } from '../services/rules.service'; import { AppsState } from './apps.state'; interface Snapshot extends LoadingState { // The current rules. - rules: ReadonlyArray; + rules: ReadonlyArray; // The selected rule. - selectedRule?: RuleDto | null; + selectedRule?: DynamicRuleDto | null; // The id of the rule that is currently running. runningRuleId?: string; @@ -81,7 +82,7 @@ export class RulesState extends State { debug(this, 'rules'); } - public select(id: string | null): Observable { + public select(id: string | null): Observable { return this.loadIfNotLoaded().pipe( map(() => this.snapshot.rules.find(x => x.id === id) || null), tap(selectedRule => { @@ -141,7 +142,7 @@ export class RulesState extends State { shareSubscribed(this.dialogs)); } - public create(request: UpsertRuleDto): Observable { + public create(request: DynamicCreateRuleDto): Observable { return this.rulesService.postRule(this.appName, request).pipe( tap(created => { this.next(s => { @@ -153,7 +154,7 @@ export class RulesState extends State { shareSubscribed(this.dialogs)); } - public delete(rule: RuleDto): Observable { + public delete(rule: DynamicRuleDto): Observable { return this.rulesService.deleteRule(this.appName, rule, rule.version).pipe( tap(() => { this.next(s => { @@ -170,15 +171,15 @@ export class RulesState extends State { shareSubscribed(this.dialogs)); } - public update(rule: RuleDto, dto: Partial): Observable { - return this.rulesService.putRule(this.appName, rule, dto, rule.version).pipe( + public update(rule: DynamicRuleDto, request: DynamicUpdateRuleDto): Observable { + return this.rulesService.putRule(this.appName, rule, request, rule.version).pipe( tap(updated => { this.replaceRule(updated); }), shareSubscribed(this.dialogs)); } - public run(rule: RuleDto): Observable { + public run(rule: DynamicRuleDto): Observable { return this.rulesService.runRule(this.appName, rule).pipe( tap(() => { this.dialogs.notifyInfo('i18n:rules.restarted'); @@ -186,7 +187,7 @@ export class RulesState extends State { shareSubscribed(this.dialogs)); } - public runFromSnapshots(rule: RuleDto): Observable { + public runFromSnapshots(rule: DynamicRuleDto): Observable { return this.rulesService.runRuleFromSnapshots(this.appName, rule).pipe( tap(() => { this.dialogs.notifyInfo('i18n:rules.restarted'); @@ -194,7 +195,7 @@ export class RulesState extends State { shareSubscribed(this.dialogs)); } - public trigger(rule: RuleDto): Observable { + public trigger(rule: DynamicRuleDto): Observable { return this.rulesService.triggerRule(this.appName, rule).pipe( tap(() => { this.dialogs.notifyInfo('i18n:rules.enqueued'); @@ -210,7 +211,7 @@ export class RulesState extends State { shareSubscribed(this.dialogs)); } - private replaceRule(rule: RuleDto) { + private replaceRule(rule: DynamicRuleDto) { this.next(s => { const rules = s.rules.replacedBy('id', rule); diff --git a/frontend/src/app/shared/state/schema-tag-source.ts b/frontend/src/app/shared/state/schema-tag-source.ts index 73e019d56..a0e8362c2 100644 --- a/frontend/src/app/shared/state/schema-tag-source.ts +++ b/frontend/src/app/shared/state/schema-tag-source.ts @@ -8,7 +8,7 @@ import { Injectable } from '@angular/core'; import { map } from 'rxjs/operators'; import { TagConverter, TagValue } from '@app/framework'; -import { SchemaDto } from '../services/schemas.service'; +import { SchemaDto } from '../model'; import { SchemasState } from './schemas.state'; class SchemaConverter implements TagConverter { diff --git a/frontend/src/app/shared/state/schemas.forms.ts b/frontend/src/app/shared/state/schemas.forms.ts index 1a3cd0f05..3c7363dd3 100644 --- a/frontend/src/app/shared/state/schemas.forms.ts +++ b/frontend/src/app/shared/state/schemas.forms.ts @@ -10,8 +10,7 @@ import { AbstractControl, UntypedFormControl, Validators } from '@angular/forms'; import { map } from 'rxjs/operators'; import { ExtendedFormGroup, Form, TemplatedFormArray, ValidatorsEx, value$ } from '@app/framework'; -import { AddFieldDto, CreateSchemaDto, FieldRule, SchemaDto, SchemaPropertiesDto, SynchronizeSchemaDto, UpdateSchemaDto } from '../services/schemas.service'; -import { createProperties, FieldPropertiesDto, FieldPropertiesVisitor } from '../services/schemas.types'; +import { AddFieldDto, ConfigureFieldRulesDto, createProperties, CreateSchemaDto, FieldPropertiesDto, FieldPropertiesVisitor, FieldRuleDto, SchemaDto, SchemaPropertiesDto, SynchronizeSchemaDto, UpdateSchemaDto } from '../model'; type CreateCategoryFormType = { name: string }; @@ -51,10 +50,10 @@ export class CreateSchemaForm extends Form { return { name, type, importing, initialCategory: category }; } - public transformSubmit(value: any): CreateSchemaDto { + public transformSubmit(value: any) { const { name, type, importing, initialCategory } = value; - return { name, type, category: initialCategory, ...importing }; + return new CreateSchemaDto({ name, type, category: initialCategory, ...importing }); } } @@ -78,15 +77,15 @@ export class SynchronizeSchemaForm extends Form, SchemaDto> { +export class ConfigureFieldRulesForm extends Form { public get rulesControls(): ReadonlyArray { return this.form.controls as any; } @@ -106,6 +105,10 @@ export class ConfigureFieldRulesForm extends Form) { return value.fieldRules || []; } + + public transformSubmit(value: any[]) { + return new ConfigureFieldRulesDto({ fieldRules: value.map(x => new FieldRuleDto(x)) }); + } } class FieldRuleTemplate { @@ -126,9 +129,7 @@ class FieldRuleTemplate { } } -type ConfigurePreviewUrlsFormType = { [name: string]: string }; - -export class ConfigurePreviewUrlsForm extends Form { +export class ConfigurePreviewUrlsForm extends Form, SchemaDto> { public get previewControls(): ReadonlyArray { return this.form.controls as any; } @@ -412,9 +413,13 @@ export class EditSchemaForm extends Form { +export class AddFieldForm extends Form> { public isContentField = value$(this.form.controls['type']).pipe(map(x => x !== 'UI')); constructor() { @@ -452,6 +457,6 @@ export class AddFieldForm extends Form { const properties = createProperties(type); const partitioning = isLocalizable ? 'language' : 'invariant'; - return { name, partitioning, properties }; + return new AddFieldDto({ name, partitioning, properties }); } } diff --git a/frontend/src/app/shared/state/schemas.state.spec.ts b/frontend/src/app/shared/state/schemas.state.spec.ts index 85807711b..f274ec4cd 100644 --- a/frontend/src/app/shared/state/schemas.state.spec.ts +++ b/frontend/src/app/shared/state/schemas.state.spec.ts @@ -6,8 +6,9 @@ */ import { firstValueFrom, of, onErrorResumeNextWith, throwError } from 'rxjs'; +import { customMatchers } from 'src/spec/matchers'; import { IMock, It, Mock, Times } from 'typemoq'; -import { DialogService, SchemaDto, SchemasService, UpdateSchemaCategoryDto, versioned } from '@app/shared/internal'; +import { AddFieldDto, ChangeCategoryDto, ConfigureUIFieldsDto, CreateSchemaDto, DialogService, SchemaDto, SchemasDto, SchemasService, SynchronizeSchemaDto, UpdateFieldDto, UpdateSchemaDto, versioned } from '@app/shared/internal'; import { createSchema } from '../services/schemas.service.spec'; import { TestValues } from './_test-helpers'; import { getCategoryTree, SchemasState } from './schemas.state'; @@ -17,25 +18,27 @@ describe('SchemasState', () => { app, appsState, newVersion, - version, } = TestValues; const schema1 = createSchema(1); const schema2 = createSchema(2); - const oldSchemas = { - canCreate: true, + const oldSchemas = new SchemasDto({ items: [ schema1, schema2, ], _links: {}, - }; + }); let dialogs: IMock; let schemasService: IMock; let schemasState: SchemasState; + beforeAll(function () { + jasmine.addMatchers(customMatchers); + }); + beforeEach(() => { dialogs = Mock.ofType(); @@ -54,7 +57,7 @@ describe('SchemasState', () => { schemasState.load().subscribe(); - expect(schemasState.snapshot.schemas).toEqual(oldSchemas.items); + expect(schemasState.snapshot.schemas).toEqualIgnoringProps(oldSchemas.items); expect(schemasState.snapshot.isLoaded).toBeTruthy(); schemasService.verifyAll(); @@ -69,7 +72,7 @@ describe('SchemasState', () => { expect(schemasState.snapshot.isLoaded).toBeTruthy(); expect(schemasState.snapshot.isLoading).toBeFalsy(); - expect(schemasState.snapshot.schemas).toEqual(oldSchemas.items); + expect(schemasState.snapshot.schemas).toEqualIgnoringProps(oldSchemas.items); schemasService.verifyAll(); }); @@ -130,14 +133,14 @@ describe('SchemasState', () => { it('should add category', () => { schemasState.addCategory('schema-category3'); - expect([...schemasState.snapshot.addedCategories]).toEqual(['schema-category3']); + expect([...schemasState.snapshot.addedCategories]).toEqualIgnoringProps(['schema-category3']); }); it('should remove category', () => { schemasState.addCategory('schema-category3'); schemasState.removeCategory('schema-category3'); - expect([...schemasState.snapshot.addedCategories]).toEqual([]); + expect([...schemasState.snapshot.addedCategories]).toEqualIgnoringProps([]); }); it('should return schema on select and reload if already loaded', () => { @@ -171,35 +174,35 @@ describe('SchemasState', () => { it('should update schema if schema published', () => { const updated = createSchema(1, '_new'); - schemasService.setup(x => x.publishSchema(app, schema1, version)) + schemasService.setup(x => x.publishSchema(app, schema1, schema1.version)) .returns(() => of(updated)).verifiable(); schemasState.publish(schema1).subscribe(); - expect(schemasState.snapshot.schemas).toEqual([updated, schema2]); + expect(schemasState.snapshot.schemas).toEqualIgnoringProps([updated, schema2]); }); it('should update schema if schema unpublished', () => { const updated = createSchema(1, '_new'); - schemasService.setup(x => x.unpublishSchema(app, schema1, version)) + schemasService.setup(x => x.unpublishSchema(app, schema1, schema1.version)) .returns(() => of(updated)).verifiable(); schemasState.unpublish(schema1).subscribe(); - expect(schemasState.snapshot.schemas).toEqual([updated, schema2]); + expect(schemasState.snapshot.schemas).toEqualIgnoringProps([updated, schema2]); }); it('should update schema if schema category changed', () => { - const category = 'my-new-category'; - const updated = createSchema(1, '_new'); - schemasService.setup(x => x.putCategory(app, schema1, It.is(i => i.name === category), version)) + const request = new ChangeCategoryDto({ name: 'my-new-category' }); + + schemasService.setup(x => x.putCategory(app, schema1, It.isValue(request), schema1.version)) .returns(() => of(updated)).verifiable(); - schemasState.changeCategory(schema1, category).subscribe(); + schemasState.changeCategory(schema1, request.name!).subscribe(); - expect(schemasState.snapshot.schemas).toEqual([updated, schema2]); + expect(schemasState.snapshot.schemas).toEqualIgnoringProps([updated, schema2]); }); describe('with selection', () => { @@ -213,55 +216,55 @@ describe('SchemasState', () => { it('should update schema and selected schema if schema published', () => { const updated = createSchema(1, '_new'); - schemasService.setup(x => x.publishSchema(app, schema1, version)) + schemasService.setup(x => x.publishSchema(app, schema1, schema1.version)) .returns(() => of(updated)).verifiable(); schemasState.publish(schema1).subscribe(); - expect(schemasState.snapshot.schemas).toEqual([updated, schema2]); - expect(schemasState.snapshot.selectedSchema).toEqual(updated); + expect(schemasState.snapshot.schemas).toEqualIgnoringProps([updated, schema2]); + expect(schemasState.snapshot.selectedSchema).toEqualIgnoringProps(updated); }); it('should update schema and selected schema if schema category changed', () => { - const category = 'my-new-category'; - const updated = createSchema(1, '_new'); - schemasService.setup(x => x.putCategory(app, schema1, It.is(i => i.name === category), version)) + const request = new ChangeCategoryDto({ name: 'my-new-category' }); + + schemasService.setup(x => x.putCategory(app, schema1, It.isValue(request), schema1.version)) .returns(() => of(updated)).verifiable(); - schemasState.changeCategory(schema1, category).subscribe(); + schemasState.changeCategory(schema1, request.name!).subscribe(); - expect(schemasState.snapshot.schemas).toEqual([updated, schema2]); - expect(schemasState.snapshot.selectedSchema).toEqual(updated); + expect(schemasState.snapshot.schemas).toEqualIgnoringProps([updated, schema2]); + expect(schemasState.snapshot.selectedSchema).toEqualIgnoringProps(updated); }); it('should update schema and selected schema if schema updated', () => { - const request = { label: 'name2_label', hints: 'name2_hints' }; + const request = new UpdateSchemaDto({ label: 'name2_label', hints: 'name2_hints' }); const updated = createSchema(1, '_new'); - schemasService.setup(x => x.putSchema(app, schema1, It.isAny(), version)) + schemasService.setup(x => x.putSchema(app, schema1, It.isAny(), schema1.version)) .returns(() => of(updated)).verifiable(); schemasState.update(schema1, request).subscribe(); - expect(schemasState.snapshot.schemas).toEqual([updated, schema2]); - expect(schemasState.snapshot.selectedSchema).toEqual(updated); + expect(schemasState.snapshot.schemas).toEqualIgnoringProps([updated, schema2]); + expect(schemasState.snapshot.selectedSchema).toEqualIgnoringProps(updated); }); it('should update schema and selected schema if schema synced', () => { - const request = {}; + const request = new SynchronizeSchemaDto({}); const updated = createSchema(1, '_new'); - schemasService.setup(x => x.putSchemaSync(app, schema1, It.isAny(), version)) + schemasService.setup(x => x.putSchemaSync(app, schema1, It.isAny(), schema1.version)) .returns(() => of(updated)).verifiable(); schemasState.synchronize(schema1, request).subscribe(); - expect(schemasState.snapshot.schemas).toEqual([updated, schema2]); - expect(schemasState.snapshot.selectedSchema).toEqual(updated); + expect(schemasState.snapshot.schemas).toEqualIgnoringProps([updated, schema2]); + expect(schemasState.snapshot.selectedSchema).toEqualIgnoringProps(updated); }); it('should update schema and selected schema if scripts configured', () => { @@ -269,13 +272,13 @@ describe('SchemasState', () => { const updated = createSchema(1, '_new'); - schemasService.setup(x => x.putScripts(app, schema1, It.isAny(), version)) + schemasService.setup(x => x.putScripts(app, schema1, It.isAny(), schema1.version)) .returns(() => of(updated)).verifiable(); schemasState.configureScripts(schema1, request).subscribe(); - expect(schemasState.snapshot.schemas).toEqual([updated, schema2]); - expect(schemasState.snapshot.selectedSchema).toEqual(updated); + expect(schemasState.snapshot.schemas).toEqualIgnoringProps([updated, schema2]); + expect(schemasState.snapshot.selectedSchema).toEqualIgnoringProps(updated); }); it('should update schema and selected schema if preview urls configured', () => { @@ -283,17 +286,17 @@ describe('SchemasState', () => { const updated = createSchema(1, '_new'); - schemasService.setup(x => x.putPreviewUrls(app, schema1, It.isAny(), version)) + schemasService.setup(x => x.putPreviewUrls(app, schema1, It.isAny(), schema1.version)) .returns(() => of(updated)).verifiable(); schemasState.configurePreviewUrls(schema1, request).subscribe(); - expect(schemasState.snapshot.schemas).toEqual([updated, schema2]); - expect(schemasState.snapshot.selectedSchema).toEqual(updated); + expect(schemasState.snapshot.schemas).toEqualIgnoringProps([updated, schema2]); + expect(schemasState.snapshot.selectedSchema).toEqualIgnoringProps(updated); }); it('should add schema to snapshot if created', () => { - const request = { name: 'newName' }; + const request = new CreateSchemaDto({ name: 'newName' }); const updated = createSchema(3, '_new'); @@ -303,11 +306,11 @@ describe('SchemasState', () => { schemasState.create(request).subscribe(); expect(schemasState.snapshot.schemas.length).toBe(3); - expect(schemasState.snapshot.schemas[2]).toEqual(updated); + expect(schemasState.snapshot.schemas[2]).toEqualIgnoringProps(updated); }); it('should remove schema from snapshot if deleted', () => { - schemasService.setup(x => x.deleteSchema(app, schema1, version)) + schemasService.setup(x => x.deleteSchema(app, schema1, schema1.version)) .returns(() => of(versioned(newVersion))).verifiable(); schemasState.delete(schema1).subscribe(); @@ -317,59 +320,59 @@ describe('SchemasState', () => { }); it('should update schema and selected schema if field added', async () => { - const request = { ...schema1.fields[0] }; + const request = new AddFieldDto({ ...schema1.fields[0] }); const updated = createSchema(1, '_new'); - schemasService.setup(x => x.postField(app, schema1, It.isAny(), version)) + schemasService.setup(x => x.postField(app, schema1, request, schema1.version)) .returns(() => of(updated)).verifiable(); const schemaField = await firstValueFrom(schemasState.addField(schema1, request)); expect(schemaField).toBeDefined(); - expect(schemasState.snapshot.schemas).toEqual([updated, schema2]); - expect(schemasState.snapshot.selectedSchema).toEqual(updated); + expect(schemasState.snapshot.schemas).toEqualIgnoringProps([updated, schema2]); + expect(schemasState.snapshot.selectedSchema).toEqualIgnoringProps(updated); }); it('should update schema and selected schema if nested field added', async () => { - const request = { ...schema1.fields[0].nested[0] }; + const request = new AddFieldDto({ ...schema1.fields[0].nested![0] }); const updated = createSchema(1, '_new'); - schemasService.setup(x => x.postField(app, schema1.fields[0], It.isAny(), version)) + schemasService.setup(x => x.postField(app, schema1.fields[0], request, schema1.version)) .returns(() => of(updated)).verifiable(); const schemaField = await firstValueFrom(schemasState.addField(schema1, request, schema1.fields[0])); expect(schemaField).toBeDefined(); - expect(schemasState.snapshot.schemas).toEqual([updated, schema2]); - expect(schemasState.snapshot.selectedSchema).toEqual(updated); + expect(schemasState.snapshot.schemas).toEqualIgnoringProps([updated, schema2]); + expect(schemasState.snapshot.selectedSchema).toEqualIgnoringProps(updated); }); it('should update schema and selected schema if field removed', () => { const updated = createSchema(1, '_new'); - schemasService.setup(x => x.deleteField(app, schema1.fields[0], version)) + schemasService.setup(x => x.deleteField(app, schema1.fields[0], schema1.version)) .returns(() => of(updated)).verifiable(); schemasState.deleteField(schema1, schema1.fields[0]).subscribe(); - expect(schemasState.snapshot.schemas).toEqual([updated, schema2]); - expect(schemasState.snapshot.selectedSchema).toEqual(updated); + expect(schemasState.snapshot.schemas).toEqualIgnoringProps([updated, schema2]); + expect(schemasState.snapshot.selectedSchema).toEqualIgnoringProps(updated); }); it('should update schema and selected schema if ui fields configured', () => { - const request = { fieldsInLists: [schema1.fields[1].name] }; + const request = new ConfigureUIFieldsDto({ fieldsInLists: [schema1.fields[1].name] }); const updated = createSchema(1, '_new'); - schemasService.setup(x => x.putUIFields(app, schema1, request, version)) + schemasService.setup(x => x.putUIFields(app, schema1, request, schema1.version)) .returns(() => of(updated)).verifiable(); schemasState.configureUIFields(schema1, request).subscribe(); - expect(schemasState.snapshot.schemas).toEqual([updated, schema2]); - expect(schemasState.snapshot.selectedSchema).toEqual(updated); + expect(schemasState.snapshot.schemas).toEqualIgnoringProps([updated, schema2]); + expect(schemasState.snapshot.selectedSchema).toEqualIgnoringProps(updated); }); it('should update schema and selected schema if fields sorted', () => { @@ -377,13 +380,13 @@ describe('SchemasState', () => { const updated = createSchema(1, '_new'); - schemasService.setup(x => x.putFieldOrdering(app, schema1, request.map(f => f.fieldId), version)) + schemasService.setup(x => x.putFieldOrdering(app, schema1, request.map(f => f.fieldId), schema1.version)) .returns(() => of(updated)).verifiable(); schemasState.orderFields(schema1, request).subscribe(); - expect(schemasState.snapshot.schemas).toEqual([updated, schema2]); - expect(schemasState.snapshot.selectedSchema).toEqual(updated); + expect(schemasState.snapshot.schemas).toEqualIgnoringProps([updated, schema2]); + expect(schemasState.snapshot.selectedSchema).toEqualIgnoringProps(updated); }); it('should update schema and selected schema if nested fields sorted', () => { @@ -391,99 +394,101 @@ describe('SchemasState', () => { const updated = createSchema(1, '_new'); - schemasService.setup(x => x.putFieldOrdering(app, schema1.fields[0], request.map(f => f.fieldId), version)) + schemasService.setup(x => x.putFieldOrdering(app, schema1.fields[0], request.map(f => f.fieldId), schema1.version)) .returns(() => of(updated)).verifiable(); schemasState.orderFields(schema1, request, schema1.fields[0]).subscribe(); - expect(schemasState.snapshot.schemas).toEqual([updated, schema2]); - expect(schemasState.snapshot.selectedSchema).toEqual(updated); + expect(schemasState.snapshot.schemas).toEqualIgnoringProps([updated, schema2]); + expect(schemasState.snapshot.selectedSchema).toEqualIgnoringProps(updated); }); it('should update schema and selected schema if field updated', () => { const updated = createSchema(1, '_new'); - const request = { ...schema1.fields[0] }; + const request = new UpdateFieldDto({ ...schema1.fields[0] }); - schemasService.setup(x => x.putField(app, schema1.fields[0], request, version)) + schemasService.setup(x => x.putField(app, schema1.fields[0], request, schema1.version)) .returns(() => of(updated)).verifiable(); schemasState.updateField(schema1, schema1.fields[0], request).subscribe(); - expect(schemasState.snapshot.schemas).toEqual([updated, schema2]); - expect(schemasState.snapshot.selectedSchema).toEqual(updated); + expect(schemasState.snapshot.schemas).toEqualIgnoringProps([updated, schema2]); + expect(schemasState.snapshot.selectedSchema).toEqualIgnoringProps(updated); }); it('should update schema and selected schema if field hidden', () => { const updated = createSchema(1, '_new'); - schemasService.setup(x => x.hideField(app, schema1.fields[0], version)) + schemasService.setup(x => x.hideField(app, schema1.fields[0], schema1.version)) .returns(() => of(updated)).verifiable(); schemasState.hideField(schema1, schema1.fields[0]).subscribe(); - expect(schemasState.snapshot.schemas).toEqual([updated, schema2]); - expect(schemasState.snapshot.selectedSchema).toEqual(updated); + expect(schemasState.snapshot.schemas).toEqualIgnoringProps([updated, schema2]); + expect(schemasState.snapshot.selectedSchema).toEqualIgnoringProps(updated); }); it('should update schema and selected schema if field disabled', () => { const updated = createSchema(1, '_new'); - schemasService.setup(x => x.disableField(app, schema1.fields[0], version)) + schemasService.setup(x => x.disableField(app, schema1.fields[0], schema1.version)) .returns(() => of(updated)); schemasState.disableField(schema1, schema1.fields[0]).subscribe(); - expect(schemasState.snapshot.schemas).toEqual([updated, schema2]); - expect(schemasState.snapshot.selectedSchema).toEqual(updated); + expect(schemasState.snapshot.schemas).toEqualIgnoringProps([updated, schema2]); + expect(schemasState.snapshot.selectedSchema).toEqualIgnoringProps(updated); }); it('should update schema and selected schema if field locked', () => { const updated = createSchema(1, '_new'); - schemasService.setup(x => x.lockField(app, schema1.fields[0], version)) + schemasService.setup(x => x.lockField(app, schema1.fields[0], schema1.version)) .returns(() => of(updated)).verifiable(); schemasState.lockField(schema1, schema1.fields[0]).subscribe(); - expect(schemasState.snapshot.schemas).toEqual([updated, schema2]); - expect(schemasState.snapshot.selectedSchema).toEqual(updated); + expect(schemasState.snapshot.schemas).toEqualIgnoringProps([updated, schema2]); + expect(schemasState.snapshot.selectedSchema).toEqualIgnoringProps(updated); }); it('should update schema and selected schema if field shown', () => { const updated = createSchema(1, '_new'); - schemasService.setup(x => x.showField(app, schema1.fields[0], version)) + schemasService.setup(x => x.showField(app, schema1.fields[0], schema1.version)) .returns(() => of(updated)).verifiable(); schemasState.showField(schema1, schema1.fields[0]).subscribe(); - expect(schemasState.snapshot.schemas).toEqual([updated, schema2]); - expect(schemasState.snapshot.selectedSchema).toEqual(updated); + expect(schemasState.snapshot.schemas).toEqualIgnoringProps([updated, schema2]); + expect(schemasState.snapshot.selectedSchema).toEqualIgnoringProps(updated); }); it('should update schema and selected schema if field enabled', () => { const updated = createSchema(1, '_new'); - schemasService.setup(x => x.enableField(app, schema1.fields[0], version)) + schemasService.setup(x => x.enableField(app, schema1.fields[0], schema1.version)) .returns(() => of(updated)).verifiable(); schemasState.enableField(schema1, schema1.fields[0]).subscribe(); - expect(schemasState.snapshot.schemas).toEqual([updated, schema2]); - expect(schemasState.snapshot.selectedSchema).toEqual(updated); + expect(schemasState.snapshot.schemas).toEqualIgnoringProps([updated, schema2]); + expect(schemasState.snapshot.selectedSchema).toEqualIgnoringProps(updated); }); it('should update schema with matching category', () => { const updated = createSchema(1, '_new'); - schemasService.setup(x => x.putCategory(app, schema1, { name: 'new-name' }, version)) + const request = new ChangeCategoryDto({ name: 'new-name' }); + + schemasService.setup(x => x.putCategory(app, schema1, It.isValue(request), schema1.version)) .returns(() => of(updated)).verifiable(); - schemasState.renameCategory('schema-category1', 'new-name').subscribe(); + schemasState.renameCategory('schema-category1', request.name!).subscribe(); - expect(schemasState.snapshot.schemas).toEqual([updated, schema2]); - expect(schemasState.snapshot.selectedSchema).toEqual(updated); + expect(schemasState.snapshot.schemas).toEqualIgnoringProps([updated, schema2]); + expect(schemasState.snapshot.selectedSchema).toEqualIgnoringProps(updated); }); }); }); @@ -499,7 +504,7 @@ describe('SchemasState', () => { const result = getCategoryTree([schemaDefault, schemaComponent], new Set()); - expect(result).toEqual([ + expect(result).toEqualIgnoringProps([ { displayName: 'i18n:common.components', schemas: [schemaComponent], @@ -522,7 +527,7 @@ describe('SchemasState', () => { it('should be build from schemas with defined categories', () => { const result = getCategoryTree([schema1, schema2], new Set()); - expect(result).toEqual([ + expect(result).toEqualIgnoringProps([ { displayName: 'i18n:common.components', schemas: [], @@ -563,7 +568,7 @@ describe('SchemasState', () => { it('should be build from schemas and custom name', () => { const result = getCategoryTree([schema1, schema2], new Set(['schema-category3'])); - expect(result).toEqual([ + expect(result).toEqualIgnoringProps([ { displayName: 'i18n:common.components', schemas: [], @@ -613,7 +618,7 @@ describe('SchemasState', () => { it('should be build from schemas and filter', () => { const result = getCategoryTree([schema1, schema2], new Set(), '1'); - expect(result).toEqual([ + expect(result).toEqualIgnoringProps([ { displayName: 'i18n:common.components', schemas: [], @@ -660,7 +665,7 @@ describe('SchemasState', () => { const result = getCategoryTree([schema1, schema2, schemaA, schemaAB], new Set()); - expect(result).toEqual([ + expect(result).toEqualIgnoringProps([ { displayName: 'i18n:common.components', schemas: [], @@ -714,7 +719,7 @@ describe('SchemasState', () => { it('should be build from schemas and custom name with nested categories', () => { const result = getCategoryTree([schema1, schema2], new Set(['A/B'])); - expect(result).toEqual([ + expect(result).toEqualIgnoringProps([ { displayName: 'i18n:common.components', schemas: [], @@ -775,7 +780,7 @@ describe('SchemasState', () => { const result = getCategoryTree([schema1, schema2, schemaA, schemaAB], new Set(), '4'); - expect(result).toEqual([ + expect(result).toEqualIgnoringProps([ { displayName: 'i18n:common.components', schemas: [], diff --git a/frontend/src/app/shared/state/schemas.state.ts b/frontend/src/app/shared/state/schemas.state.ts index b11d9aeca..846d318ab 100644 --- a/frontend/src/app/shared/state/schemas.state.ts +++ b/frontend/src/app/shared/state/schemas.state.ts @@ -9,10 +9,11 @@ import { Injectable } from '@angular/core'; import { EMPTY, forkJoin, Observable, of } from 'rxjs'; import { catchError, finalize, tap } from 'rxjs/operators'; import { debug, DialogService, LoadingState, shareMapSubscribed, shareSubscribed, State, Version } from '@app/framework'; -import { AddFieldDto, CreateSchemaDto, FieldDto, FieldRule, NestedFieldDto, RootFieldDto, SchemaDto, SchemasService, UpdateFieldDto, UpdateSchemaDto, UpdateUIFields } from '../services/schemas.service'; +import { AddFieldDto, ChangeCategoryDto, ConfigureFieldRulesDto, ConfigureUIFieldsDto, CreateSchemaDto, FieldDto, NestedFieldDto, SchemaDto, SynchronizeSchemaDto, UpdateFieldDto, UpdateSchemaDto } from '../model'; +import { SchemasService } from '../services/schemas.service'; import { AppsState } from './apps.state'; -type AnyFieldDto = NestedFieldDto | RootFieldDto; +type AnyFieldDto = FieldDto | NestedFieldDto; interface Snapshot extends LoadingState { // The schema categories. @@ -193,7 +194,7 @@ export class SchemasState extends State { public renameCategory(oldName: string, name: string) { const schemas = this.snapshot.schemas.filter(x => x.category === oldName); - return forkJoin(schemas.map(s => this.schemasService.putCategory(this.appName, s, { name }, s.version))).pipe( + return forkJoin(schemas.map(s => this.schemasService.putCategory(this.appName, s, new ChangeCategoryDto({ name }), s.version))).pipe( tap(updated => { this.next(s => { let { schemas, selectedSchema } = s; @@ -232,14 +233,14 @@ export class SchemasState extends State { } public changeCategory(schema: SchemaDto, name?: string): Observable { - return this.schemasService.putCategory(this.appName, schema, { name }, schema.version).pipe( + return this.schemasService.putCategory(this.appName, schema, new ChangeCategoryDto({ name }), schema.version).pipe( tap(updated => { this.replaceSchema(updated); }), shareSubscribed(this.dialogs)); } - public configurePreviewUrls(schema: SchemaDto, request: {}): Observable { + public configurePreviewUrls(schema: SchemaDto, request: Record): Observable { return this.schemasService.putPreviewUrls(this.appName, schema, request, schema.version).pipe( tap(updated => { this.replaceSchema(updated, schema.version, 'i18n:schemas.saved'); @@ -247,15 +248,15 @@ export class SchemasState extends State { shareSubscribed(this.dialogs)); } - public configureFieldRules(schema: SchemaDto, request: ReadonlyArray): Observable { - return this.schemasService.putFieldRules(this.appName, schema, request, schema.version).pipe( + public configureFieldRules(schema: SchemaDto, request: ConfigureFieldRulesDto): Observable { + return this.schemasService.putFieldRules(this.appName, schema, { fieldRules: request } as any, schema.version).pipe( tap(updated => { this.replaceSchema(updated, schema.version, 'i18n:schemas.saved'); }), shareSubscribed(this.dialogs)); } - public configureScripts(schema: SchemaDto, request: {}): Observable { + public configureScripts(schema: SchemaDto, request: Record): Observable { return this.schemasService.putScripts(this.appName, schema, request, schema.version).pipe( tap(updated => { this.replaceSchema(updated, schema.version, 'i18n:schemas.saved'); @@ -263,7 +264,7 @@ export class SchemasState extends State { shareSubscribed(this.dialogs)); } - public synchronize(schema: SchemaDto, request: {}): Observable { + public synchronize(schema: SchemaDto, request: SynchronizeSchemaDto): Observable { return this.schemasService.putSchemaSync(this.appName, schema, request, schema.version).pipe( tap(updated => { this.replaceSchema(updated, schema.version, 'i18n:schemas.synchronized'); @@ -279,7 +280,7 @@ export class SchemasState extends State { shareSubscribed(this.dialogs)); } - public addField(schema: SchemaDto, request: AddFieldDto, parent?: RootFieldDto | null): Observable { + public addField(schema: SchemaDto, request: AddFieldDto, parent?: FieldDto | null): Observable { return this.schemasService.postField(this.appName, parent || schema, request, schema.version).pipe( tap(updated => { this.replaceSchema(updated); @@ -287,7 +288,7 @@ export class SchemasState extends State { shareMapSubscribed(this.dialogs, x => getField(x, request, parent), { silent: true })); } - public configureUIFields(schema: SchemaDto, request: UpdateUIFields): Observable { + public configureUIFields(schema: SchemaDto, request: ConfigureUIFieldsDto): Observable { return this.schemasService.putUIFields(this.appName, schema, request, schema.version).pipe( tap(updated => { this.replaceSchema(updated, schema.version); @@ -295,7 +296,7 @@ export class SchemasState extends State { shareSubscribed(this.dialogs)); } - public orderFields(schema: SchemaDto, fields: ReadonlyArray, parent?: RootFieldDto): Observable { + public orderFields(schema: SchemaDto, fields: ReadonlyArray, parent?: FieldDto): Observable { return this.schemasService.putFieldOrdering(this.appName, parent || schema, fields.map(t => t.fieldId), schema.version).pipe( tap(updated => { this.replaceSchema(updated, schema.version); @@ -360,7 +361,7 @@ export class SchemasState extends State { } private replaceSchema(schema: SchemaDto, oldVersion?: Version, updateText?: string) { - if (!oldVersion || !oldVersion.eq(schema.version)) { + if (!oldVersion || oldVersion !== schema.version) { if (updateText) { this.dialogs.notifyInfo(updateText); } @@ -383,9 +384,9 @@ export class SchemasState extends State { } } -function getField(x: SchemaDto, request: AddFieldDto, parent?: RootFieldDto | null): FieldDto { +function getField(x: SchemaDto, request: AddFieldDto, parent?: FieldDto | null): AnyFieldDto { if (parent) { - return x.fields.find(f => f.fieldId === parent.fieldId)!.nested.find(f => f.name === request.name)!; + return x.fields.find(f => f.fieldId === parent.fieldId)!.nested?.find(f => f.name === request.name)!; } else { return x.fields.find(f => f.name === request.name)!; } @@ -394,7 +395,7 @@ function getField(x: SchemaDto, request: AddFieldDto, parent?: RootFieldDto | nu function buildCategoryNames(categories: Set, allSchemas: ReadonlyArray): ReadonlyArray { const uniqueCategories: { [name: string]: boolean } = {}; - function addCategory(name: string) { + function addCategory(name?: string) { if (name) { uniqueCategories[name] = true; } @@ -447,7 +448,7 @@ export function getCategoryTree(allSchemas: ReadonlyArray, categories schemasFiltered: [], countSchemasInSubtree: 0, countSchemasInSubtreeFiltered: 0, - categories: [] + categories: [], }; const components: SchemaCategory = { @@ -456,7 +457,7 @@ export function getCategoryTree(allSchemas: ReadonlyArray, categories schemasFiltered: [], countSchemasInSubtree: 0, countSchemasInSubtreeFiltered: 0, - categories: [] + categories: [], }; const categoryCache: { [name: string]: SchemaCategory } = {}; diff --git a/frontend/src/app/shared/state/table-settings.spec.ts b/frontend/src/app/shared/state/table-settings.spec.ts index de75e94a0..8ab5b2308 100644 --- a/frontend/src/app/shared/state/table-settings.spec.ts +++ b/frontend/src/app/shared/state/table-settings.spec.ts @@ -7,27 +7,25 @@ import { of } from 'rxjs'; import { IMock, It, Mock, Times } from 'typemoq'; -import { DateTime, Version } from '@app/framework'; -import { createProperties, FieldSizes, META_FIELDS, RootFieldDto, SchemaDto, TableSettings, UIState } from '@app/shared/internal'; +import { createProperties, FieldSizes, META_FIELDS, TableSettings, UIState } from '@app/shared/internal'; import { FieldWrappings } from '..'; +import { TestValues } from './_test-helpers'; describe('TableSettings', () => { let uiState: IMock; + const { + createSchema, + createField, + } = TestValues; + const schema = - new SchemaDto({}, - '1', - DateTime.now(), 'me', - DateTime.now(), 'me', - new Version('1'), - 'my-schema', - 'my-category', - 'Default', - false, - {}, - [ - new RootFieldDto({}, 1, 'string', createProperties('String'), 'invariant'), - ]); + createSchema({ + name: 'my-schema', + fields: [ + createField({ id: 1, properties: createProperties('String') }), + ], + }); beforeEach(() => { uiState = Mock.ofType(); diff --git a/frontend/src/app/shared/state/table-settings.ts b/frontend/src/app/shared/state/table-settings.ts index c1689329d..ce357a84c 100644 --- a/frontend/src/app/shared/state/table-settings.ts +++ b/frontend/src/app/shared/state/table-settings.ts @@ -7,7 +7,7 @@ import { map, take } from 'rxjs/operators'; import { State, Types } from '@app/framework'; -import { META_FIELDS, SchemaDto, TableField } from '../services/schemas.service'; +import { META_FIELDS, SchemaDto, TableField } from '../model'; import { UIState } from './ui.state'; const META_FIELD_NAMES = Object.values(META_FIELDS).filter(x => x !== META_FIELDS.empty); diff --git a/frontend/src/app/shared/state/teams.forms.ts b/frontend/src/app/shared/state/teams.forms.ts index f12f6d7ef..4d4a8b15f 100644 --- a/frontend/src/app/shared/state/teams.forms.ts +++ b/frontend/src/app/shared/state/teams.forms.ts @@ -9,7 +9,7 @@ import { UntypedFormControl, Validators } from '@angular/forms'; import { ExtendedFormGroup, Form } from '@app/framework'; -import { CreateTeamDto, TeamDto, UpdateTeamDto } from '../services/teams.service'; +import { CreateTeamDto, TeamDto, UpdateTeamDto } from '../model'; export class CreateTeamForm extends Form { constructor() { @@ -20,6 +20,10 @@ export class CreateTeamForm extends Form { ]), })); } + + public transformSubmit(value: any) { + return new CreateTeamDto(value); + } } export class UpdateTeamForm extends Form { @@ -31,4 +35,8 @@ export class UpdateTeamForm extends Form { @@ -77,7 +77,7 @@ describe('TeamsState', () => { it('should add team to snapshot if created', () => { const updated = createTeam(3, '_new'); - const request = { ...updated }; + const request = new CreateTeamDto({ ...updated }); teamsService.setup(x => x.postTeam(request)) .returns(() => of(updated)).verifiable(); @@ -88,10 +88,10 @@ describe('TeamsState', () => { }); it('should update team if updated', () => { - const request = {}; - const updated = createTeam(2, '_new'); + const request = new UpdateTeamDto({ name: 'NewName' }); + teamsService.setup(x => x.putTeam(team2.name, team2, request, team2.version)) .returns(() => of(updated)).verifiable(); @@ -138,10 +138,10 @@ describe('TeamsState', () => { }); it('should update selected team if updated', () => { - const request = {}; - const updated = createTeam(1, '_new'); + const request = new UpdateTeamDto({ name: 'NewName' }); + teamsService.setup(x => x.putTeam(team1.name, team1, request, team1.version)) .returns(() => of(updated)).verifiable(); diff --git a/frontend/src/app/shared/state/template.state.spec.ts b/frontend/src/app/shared/state/template.state.spec.ts index f626ac755..df87a8b2f 100644 --- a/frontend/src/app/shared/state/template.state.spec.ts +++ b/frontend/src/app/shared/state/template.state.spec.ts @@ -7,7 +7,7 @@ import { of, onErrorResumeNextWith, throwError } from 'rxjs'; import { IMock, It, Mock, Times } from 'typemoq'; -import { DialogService, TemplatesService, TemplatesState } from '@app/shared/internal'; +import { DialogService, TemplatesDto, TemplatesService, TemplatesState } from '@app/shared/internal'; import { createTemplate } from '../services/templates.service.spec'; describe('TemplatesState', () => { @@ -32,7 +32,7 @@ describe('TemplatesState', () => { describe('Loading', () => { it('should load templates', () => { templatesService.setup(x => x.getTemplates()) - .returns(() => of({ items: [template1, template2] })).verifiable(); + .returns(() => of(new TemplatesDto({ items: [template1, template2], _links: {} }))).verifiable(); templatesState.load().subscribe(); @@ -54,7 +54,7 @@ describe('TemplatesState', () => { it('should show notification on load if reload is true', () => { templatesService.setup(x => x.getTemplates()) - .returns(() => of({ items: [template1, template2] })).verifiable(); + .returns(() => of(new TemplatesDto({ items: [template1, template2], _links: {} }))).verifiable(); templatesState.load(true).subscribe(); diff --git a/frontend/src/app/shared/state/templates.state.ts b/frontend/src/app/shared/state/templates.state.ts index 3b3677ce8..5620fa8aa 100644 --- a/frontend/src/app/shared/state/templates.state.ts +++ b/frontend/src/app/shared/state/templates.state.ts @@ -9,7 +9,8 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { finalize, tap } from 'rxjs/operators'; import { debug, DialogService, LoadingState, shareSubscribed, State } from '@app/framework'; -import { TemplateDto, TemplatesService } from '../services/templates.service'; +import { TemplateDto } from '../model'; +import { TemplatesService } from '../services/templates.service'; interface Snapshot extends LoadingState { // The current templates. diff --git a/frontend/src/app/shared/state/ui.state.spec.ts b/frontend/src/app/shared/state/ui.state.spec.ts index ac23d94c2..cd5fb28be 100644 --- a/frontend/src/app/shared/state/ui.state.spec.ts +++ b/frontend/src/app/shared/state/ui.state.spec.ts @@ -7,7 +7,7 @@ import { of } from 'rxjs'; import { IMock, Mock } from 'typemoq'; -import { ResourceLinks, UIService, UIState, UsersService } from '@app/shared/internal'; +import { IResourceDto, ResourceLinkDto, UIService, UIState, UsersService } from '@app/shared/internal'; import { TestValues } from './_test-helpers'; describe('UIState', () => { @@ -41,10 +41,12 @@ describe('UIState', () => { canCustomize: true, }; - const resources: ResourceLinks = { - 'admin/events': { method: 'GET', href: '/api/events' }, - 'admin/restore': { method: 'GET', href: '/api/restore' }, - 'admin/users': { method: 'GET', href: '/api/users' }, + const resource: IResourceDto = { + _links: { + 'admin/events': new ResourceLinkDto({ method: 'GET', href: '/api/events' }), + 'admin/restore': new ResourceLinkDto({ method: 'GET', href: '/api/restore' }), + 'admin/users': new ResourceLinkDto({ method: 'GET', href: '/api/users' }), + }, }; let usersService: IMock; @@ -66,7 +68,7 @@ describe('UIState', () => { usersService = Mock.ofType(); usersService.setup(x => x.getResources()) - .returns(() => of({ _links: resources })); + .returns(() => of(resource)); uiState = new UIState(uiService.object, usersService.object); uiState.load(); diff --git a/frontend/src/app/shared/state/workflows.forms.ts b/frontend/src/app/shared/state/workflows.forms.ts index 568b35916..90d48c384 100644 --- a/frontend/src/app/shared/state/workflows.forms.ts +++ b/frontend/src/app/shared/state/workflows.forms.ts @@ -7,9 +7,9 @@ import { UntypedFormControl, Validators } from '@angular/forms'; import { ExtendedFormGroup, Form, hasNoValue$ } from '@app/framework'; -import { CreateWorkflowDto } from '../services/workflows.service'; +import { AddWorkflowDto } from '../model'; -export class AddWorkflowForm extends Form { +export class AddWorkflowForm extends Form { public get name() { return this.form.controls['name']; } @@ -23,4 +23,8 @@ export class AddWorkflowForm extends Form ), })); } + + public transformSubmit(value: any) { + return new AddWorkflowDto(value); + } } diff --git a/frontend/src/app/shared/state/workflows.state.spec.ts b/frontend/src/app/shared/state/workflows.state.spec.ts index e94ad0ac7..226520a0f 100644 --- a/frontend/src/app/shared/state/workflows.state.spec.ts +++ b/frontend/src/app/shared/state/workflows.state.spec.ts @@ -7,7 +7,7 @@ import { of, onErrorResumeNextWith, throwError } from 'rxjs'; import { IMock, It, Mock, Times } from 'typemoq'; -import { DialogService, versioned, WorkflowsPayload, WorkflowsService, WorkflowsState } from '@app/shared/internal'; +import { AddWorkflowDto, DialogService, versioned, WorkflowDto, WorkflowsDto, WorkflowsService, WorkflowsState, WorkflowView } from '@app/shared/internal'; import { createWorkflows } from '../services/workflows.service.spec'; import { TestValues } from './_test-helpers'; @@ -83,10 +83,12 @@ describe('WorkflowsState', () => { it('should update workflows if workflow added', () => { const updated = createWorkflows('1', '2', '3'); - workflowsService.setup(x => x.postWorkflow(app, { name: 'my-workflow' }, version)) + const request = new AddWorkflowDto({ name: 'my-workflow' }); + + workflowsService.setup(x => x.postWorkflow(app, request, version)) .returns(() => of(versioned(newVersion, updated))).verifiable(); - workflowsState.add('my-workflow').subscribe(); + workflowsState.add(request).subscribe(); expectNewWorkflows(updated); }); @@ -94,12 +96,12 @@ describe('WorkflowsState', () => { it('should update workflows if workflow updated', () => { const updated = createWorkflows('1', '2', '3'); - const request = oldWorkflows.items[0].serialize(); + const request = { initial: '1' } as any; workflowsService.setup(x => x.putWorkflow(app, oldWorkflows.items[0], request, version)) .returns(() => of(versioned(newVersion, updated))).verifiable(); - workflowsState.update(oldWorkflows.items[0]).subscribe(); + workflowsState.update(oldWorkflows.items[0], request).subscribe(); expectNewWorkflows(updated); @@ -117,9 +119,343 @@ describe('WorkflowsState', () => { expectNewWorkflows(updated); }); - function expectNewWorkflows(updated: WorkflowsPayload) { + function expectNewWorkflows(updated: WorkflowsDto) { expect(workflowsState.snapshot.workflows).toEqual(updated.items); expect(workflowsState.snapshot.version).toEqual(newVersion); } }); }); + +describe('Workflow', () => { + const dto = new WorkflowDto({ id: 'id', steps: {}, initial: null!, _links: {} }); + + it('should create empty workflow', () => { + const workflow = new WorkflowView(dto); + + expect(workflow.dto.initial).toBeNull(); + }); + + it('should add step to workflow', () => { + const workflow = + new WorkflowView(dto) + .setStep('Draft', { color: '#00ff00' }); + + expect(workflow.dto.toJSON()).toEqual({ + id: 'id', + initial: 'Draft', + steps: { + 'Draft': { transitions: {}, color: '#00ff00' }, + }, + _links: {}, + }); + }); + + it('should override settings if step already exists', () => { + const workflow = + new WorkflowView(dto) + .setStep('1', { color: '#00ff00', noUpdate: true }) + .setStep('1', { color: 'red' }); + + expect(workflow.dto.toJSON()).toEqual({ + id: 'id', + initial: '1', + steps: { + 1: { transitions: {}, color: 'red', noUpdate: true }, + }, + _links: {}, + }); + }); + + it('should sort steps case invariant', () => { + const workflow = + new WorkflowView(dto) + .setStep('Z') + .setStep('a'); + + expect(workflow.steps).toEqual([ + { name: 'a', values: {}, isLocked: false }, + { name: 'Z', values: {}, isLocked: false }, + ]); + }); + + it('should return same workflow if step to remove is locked', () => { + const workflow = + new WorkflowView(dto) + .setStep('Published', { color: '#00ff00' }); + + const updated = workflow.removeStep('Published'); + + expect(updated).toBe(workflow); + }); + + it('should return same workflow if step to remove not found', () => { + const workflow = + new WorkflowView(dto) + .setStep('1'); + + const updated = workflow.removeStep('3'); + + expect(updated).toBe(workflow); + }); + + it('should remove step', () => { + const workflow = + new WorkflowView(dto) + .setStep('1', { color: '#00ff00' }) + .setStep('2', { color: '#ff0000' }) + .setStep('3', { color: '#0000ff' }) + .setTransition('1', '2', { expression: '1 === 2' }) + .setTransition('1', '3', { expression: '1 === 3' }) + .setTransition('2', '3', { expression: '2 === 3' }) + .removeStep('1'); + + expect(workflow.dto.toJSON()).toEqual({ + id: 'id', + initial: '2', + steps: { + 2: { + transitions: { + 3: { expression: '2 === 3' }, + }, + color: '#ff0000', + }, + 3: { transitions: {}, color: '#0000ff' }, + }, + _links: {}, + }); + }); + + it('should make first non-locked step the initial step if initial removed', () => { + const workflow = + new WorkflowView(dto) + .setStep('1') + .setStep('Published') + .setStep('Public') + .removeStep('1'); + + expect(workflow.dto.toJSON()).toEqual({ + id: 'id', + initial: 'Public', + steps: { + Published: { transitions: {} }, + Public: { transitions: {} }, + }, + _links: {}, + }); + }); + + it('should unset initial step if initial removed', () => { + const workflow = + new WorkflowView(dto) + .setStep('1') + .removeStep('1'); + + expect(workflow.dto.toJSON()).toEqual({ + id: 'id', + initial: null, + steps: {}, + _links: {}, + }); + }); + + it('should rename step', () => { + const workflow = + new WorkflowView(dto) + .setStep('1', { color: '#00ff00' }) + .setStep('2', { color: '#ff0000' }) + .setStep('3', { color: '#0000ff' }) + .setTransition('1', '2', { expression: '1 === 2' }) + .setTransition('2', '1', { expression: '2 === 1' }) + .setTransition('2', '3', { expression: '2 === 3' }) + .renameStep('1', 'a'); + + expect(workflow.dto.toJSON()).toEqual({ + id: 'id', + initial: '1', + steps: { + a: { + transitions: { + 2: { expression: '1 === 2' }, + }, + color: '#00ff00', + }, + 2: { + transitions: { + a: { expression: '2 === 1' }, + 3: { expression: '2 === 3' }, + }, + color: '#ff0000', + }, + 3: { transitions: {}, color: '#0000ff' }, + }, + _links: {}, + }); + }); + + it('should add transitions to workflow', () => { + const workflow = + new WorkflowView(dto) + .setStep('1') + .setStep('2') + .setTransition('1', '2', { expression: '1 === 2' }) + .setTransition('2', '1', { expression: '2 === 1' }); + + expect(workflow.dto.toJSON()).toEqual({ + id: 'id', + initial: '1', + steps: { + 1: { + transitions: { + 2: { expression: '1 === 2' }, + }, + }, + 2: { + transitions: { + 1: { expression: '2 === 1' }, + }, + }, + }, + _links: {}, + }); + }); + + it('should remove transition from workflow', () => { + const workflow = + new WorkflowView(dto) + .setStep('1') + .setStep('2') + .setTransition('1', '2', { expression: '1 === 2' }) + .setTransition('2', '1', { expression: '2 === 1' }) + .removeTransition('1', '2'); + + expect(workflow.dto.toJSON()).toEqual({ + id: 'id', + initial: '1', + steps: { + 1: { transitions: {} }, + 2: { + transitions: { + 1: { expression: '2 === 1' }, + }, + }, + }, + _links: {}, + }); + }); + + it('should override settings if transition already exists', () => { + const workflow = + new WorkflowView(dto) + .setStep('1') + .setStep('2') + .setTransition('2', '1', { expression: '2 === 1', roles: ['Role'] }) + .setTransition('2', '1', { expression: '2 !== 1' }); + + expect(workflow.dto.toJSON()).toEqual({ + id: 'id', + initial: '1', + steps: { + 1: { transitions: {} }, + 2: { + transitions: { + 1: { expression: '2 !== 1', roles: ['Role'] }, + }, + }, + }, + _links: {}, + }); + }); + + it('should return same workflow if transition to update not found by from step', () => { + const workflow = + new WorkflowView(dto) + .setStep('1') + .setStep('2') + .setTransition('1', '2'); + + const updated = workflow.setTransition('3', '2', { roles: ['Role'] }); + + expect(updated).toBe(workflow); + }); + + it('should return same workflow if transition has invalid steps', () => { + const workflow = + new WorkflowView(dto) + .setStep('1') + .setStep('2') + .setTransition('1', '2'); + + const updated = workflow.setTransition('1', '3', { roles: ['Role'] }); + + expect(updated).toBe(workflow); + }); + + it('should return same workflow if transition to remove not', () => { + const workflow = + new WorkflowView(dto) + .setStep('1') + .setStep('2') + .setTransition('1', '2'); + + const updated = workflow.removeTransition('1', '3'); + + expect(updated).toBe(workflow); + }); + + it('should return same workflow if step to make initial is locked', () => { + const workflow = + new WorkflowView(dto) + .setStep('1') + .setStep('Published', { color: '#00ff00' }); + + const updated = workflow.setInitial('Published'); + + expect(updated).toBe(workflow); + }); + + it('should set initial step', () => { + const workflow = + new WorkflowView(dto) + .setStep('1') + .setStep('2') + .setInitial('2'); + + expect(workflow.dto.toJSON()).toEqual({ + id: 'id', + initial: '2', + steps: { + 1: { transitions: {} }, + 2: { transitions: {} }, + }, + _links: {}, + }); + }); + + it('should rename workflow', () => { + const workflow = + new WorkflowView(dto) + .changeName('name'); + + expect(workflow.dto.toJSON()).toEqual({ + id: 'id', + initial: null, + name: 'name', + steps: {}, + _links: {}, + }); + }); + + it('should update schemaIds', () => { + const workflow = + new WorkflowView(dto) + .changeSchemaIds(['1', '2']); + + expect(workflow.dto.toJSON()).toEqual({ + id: 'id', + initial: null, + schemaIds: ['1', '2'], + steps: {}, + _links: {}, + }); + }); +}); \ No newline at end of file diff --git a/frontend/src/app/shared/state/workflows.state.ts b/frontend/src/app/shared/state/workflows.state.ts index d23e7e5ca..8f891ea00 100644 --- a/frontend/src/app/shared/state/workflows.state.ts +++ b/frontend/src/app/shared/state/workflows.state.ts @@ -8,8 +8,9 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { finalize, tap } from 'rxjs/operators'; -import { debug, DialogService, LoadingState, shareSubscribed, State, Version } from '@app/framework'; -import { WorkflowDto, WorkflowsPayload, WorkflowsService } from '../services/workflows.service'; +import { compareStrings, debug, DialogService, LoadingState, Mutable, shareSubscribed, State, VersionTag } from '@app/framework'; +import { WorkflowsService } from '../services/workflows.service'; +import { AddWorkflowDto, IWorkflowStepDto, IWorkflowTransitionDto, UpdateWorkflowDto, WorkflowDto, WorkflowsDto, WorkflowStepDto, WorkflowTransitionDto } from './../model'; import { AppsState } from './apps.state'; interface Snapshot extends LoadingState { @@ -17,7 +18,7 @@ interface Snapshot extends LoadingState { workflows: ReadonlyArray; // The app version. - version: Version; + version: VersionTag; // The errors. errors: ReadonlyArray; @@ -58,7 +59,7 @@ export class WorkflowsState extends State { private readonly dialogs: DialogService, private readonly workflowsService: WorkflowsService, ) { - super({ errors: [], workflows: [], version: Version.EMPTY }); + super({ errors: [], workflows: [], version: VersionTag.EMPTY }); debug(this, 'workflows'); } @@ -88,16 +89,16 @@ export class WorkflowsState extends State { shareSubscribed(this.dialogs)); } - public add(name: string): Observable { - return this.workflowsService.postWorkflow(this.appName, { name }, this.version).pipe( + public add(request: AddWorkflowDto): Observable { + return this.workflowsService.postWorkflow(this.appName, request, this.version).pipe( tap(({ version, payload }) => { this.replaceWorkflows(payload, version); }), shareSubscribed(this.dialogs)); } - public update(workflow: WorkflowDto): Observable { - return this.workflowsService.putWorkflow(this.appName, workflow, workflow.serialize(), this.version).pipe( + public update(workflow: WorkflowDto, request: UpdateWorkflowDto): Observable { + return this.workflowsService.putWorkflow(this.appName, workflow, request, this.version).pipe( tap(({ version, payload }) => { this.dialogs.notifyInfo('i18n:workflows.saved'); @@ -114,7 +115,7 @@ export class WorkflowsState extends State { shareSubscribed(this.dialogs)); } - private replaceWorkflows(payload: WorkflowsPayload, version: Version) { + private replaceWorkflows(payload: WorkflowsDto, version: VersionTag) { const { canCreate, errors, items: workflows } = payload; this.next({ @@ -131,3 +132,202 @@ export class WorkflowsState extends State { return this.snapshot.version; } } + +export interface WorkflowStepView { + // The name of the workflow. + name: string; + + // The actual step. + values: WorkflowStepValues; + + // True, if the step cannot be removed. + isLocked?: boolean; +} + +export type WorkflowStepValues = Omit; + +export interface WorkflowTransitionView { + // The source step name. + from: string; + + // The target step name. + to: string; + + // The actual transition. + values: WorkflowTransitionValues; + + // The actual workflow step. + step: WorkflowStepView; +} + +export type WorkflowTransitionValues = IWorkflowTransitionDto; + +export class WorkflowView { + public steps: ReadonlyArray = []; + + public transitions: ReadonlyArray = []; + + constructor( + public readonly dto: WorkflowDto, + ) { + const resultSteps: WorkflowStepView[] = []; + const resultTransitions: WorkflowTransitionView[] = []; + + for (const [name, step] of Object.entries(dto.steps)) { + const { transitions: _, ...values } = step.toJSON(); + + resultSteps.push({ name, isLocked: isLocked(name), values }); + } + + for (const [from, step] of Object.entries(dto.steps)) { + if (step.transitions) { + for (const [to, transition] of Object.entries(step.transitions)) { + const step = resultSteps.find(x => x.name === to)!; + + resultTransitions.push({ from, to, step, values: transition.toJSON() }); + } + } + } + + this.steps = + resultSteps.sort((a, b) => { + return compareStrings(a.name, b.name); + }); + + this.transitions = + resultTransitions.sort((a, b) => { + return compareStrings(a.to, b.to); + }); + } + + public getOpenSteps(step: WorkflowStepView) { + return this.steps.filter(x => x.name !== step.name && !this.transitions.find(y => y.from === step.name && y.to === x.name)); + } + + public getTransitions(step: WorkflowStepView): WorkflowTransitionView[] { + return this.transitions.filter(x => x.from === step.name); + } + + public getStep(name: string): WorkflowStepView { + return this.steps.find(x => x.name === name)!; + } + + public setStep(name: string, values: Partial = {}) { + return this.update(clone => { + clone.steps[name] = new WorkflowStepDto({ transitions: {}, ...clone.steps[name] ?? {}, ...values }); + + if (!clone.initial && !isLocked(name)) { + clone.initial = name; + } + }); + } + + public removeStep(name: string) { + const step = this.steps.find(x => x.name === name); + if (!step || step.isLocked) { + return this; + } + + return this.update(clone => { + delete clone.steps[name]; + + if (clone.initial === name) { + clone.initial = this.steps.filter(x => x.name !== name && !x.isLocked)[0]?.name ?? null; + } + + for (const step of Object.values(clone.steps) as any[]) { + delete step.transitions[name]; + } + }); + } + + public changeSchemaIds(schemaIds: string[]) { + if (this.dto.schemaIds === schemaIds) { + return this; + } + + return this.update(clone => { + clone.schemaIds = schemaIds; + }); + } + + public changeName(name: string) { + if (this.dto.name === name) { + return this; + } + + return this.update(clone => { + clone.name = name; + }); + } + + public setInitial(initial: string) { + const step = this.steps.find(x => x.name === initial); + if (!step || step.isLocked || this.dto.initial === initial) { + return this; + } + + return this.update(clone => { + clone.initial = initial; + }); + } + + public setTransition(from: string, to: string, values: Partial = {}) { + if (!this.dto.steps[from] || !this.dto.steps[to]) { + return this; + } + + return this.update(clone => { + const step = clone.steps[from] as Mutable; + step.transitions ??= {}; + step.transitions[to] = new WorkflowTransitionDto({ ...step.transitions[to] ?? {}, ...values }); + }); + } + + public removeTransition(from: string, to: string) { + const step = this.dto.steps[from]; + if (!step || !step.transitions?.[to]) { + return this; + } + + return this.update(clone => { + delete clone.steps[from].transitions![to]; + }); + } + + public renameStep(name: string, newName: string) { + if (!this.dto.steps[name] || name === newName) { + return this; + } + + return this.update(clone => { + renameInObj(clone.steps, name, newName); + + for (const step of Object.values(clone.steps) as any[]) { + renameInObj(step.transitions, name, newName); + } + }); + } + + private update(action: (clone: Mutable) => void) { + const clone = WorkflowDto.fromJSON(this.dto.toJSON()); + action(clone); + return new WorkflowView(WorkflowDto.fromJSON(clone)); + } + + public toUpdate(): UpdateWorkflowDto { + return UpdateWorkflowDto.fromJSON(this.dto.toJSON()); + } +} + +function renameInObj(target: any, name: string, newName: string) { + const existing = target[name]; + if (existing) { + target[newName] = existing; + delete target[name]; + } +} + +function isLocked(name: string) { + return name === 'Published'; +} \ No newline at end of file diff --git a/frontend/src/app/shared/utils/editor-utils.spec.ts b/frontend/src/app/shared/utils/editor-utils.spec.ts index 8bdec4b8b..d0c6ea7bc 100644 --- a/frontend/src/app/shared/utils/editor-utils.spec.ts +++ b/frontend/src/app/shared/utils/editor-utils.spec.ts @@ -7,22 +7,31 @@ /* eslint-disable no-template-curly-in-string */ -import { Version } from '@app/framework'; -import { AppSettingsDto } from '../services/apps.service'; +import { AppSettingsDto, EditorDto } from '../model'; import { computeEditorUrl } from './editor-utils'; describe('EditorUtils', () => { - const settings = new AppSettingsDto({}, false, [], [{ - name: 'editor1', - url: 'url/to/editor1', - }, { - name: 'duplicate', - url: 'url/to/duplicate1', - }, { - name: 'duplicate', - url: 'url/to/duplicate2', - }], - new Version('1')); + const settings = new AppSettingsDto({ + patterns: [], + hideDateTimeModeButton: false, + hideScheduler: false, + editors: [ + new EditorDto({ + name: 'editor1', + url: 'url/to/editor1', + }), + new EditorDto({ + name: 'duplicate', + url: 'url/to/duplicate1', + }), + new EditorDto({ + name: 'duplicate', + url: 'url/to/duplicate2', + }), + ], + version: 1, + _links: {}, + }); it('should interpolate editor url', () => { const result = computeEditorUrl('http://${editor1}?query=value', settings); diff --git a/frontend/src/app/shared/utils/editor-utils.ts b/frontend/src/app/shared/utils/editor-utils.ts index 526eb787c..3866222b7 100644 --- a/frontend/src/app/shared/utils/editor-utils.ts +++ b/frontend/src/app/shared/utils/editor-utils.ts @@ -6,7 +6,7 @@ */ import { interpolate } from '@app/framework'; -import { AppSettingsDto } from '../services/apps.service'; +import { AppSettingsDto } from '../model'; export function computeEditorUrl(url?: string | null, settings?: AppSettingsDto | null) { if (!url) { @@ -14,7 +14,6 @@ export function computeEditorUrl(url?: string | null, settings?: AppSettingsDto } const editors: { [key: string]: string } = {}; - if (settings?.editors) { for (const editor of settings.editors) { editors[editor.name] = editor.url; diff --git a/frontend/src/spec/matchers.ts b/frontend/src/spec/matchers.ts new file mode 100644 index 000000000..404a9a996 --- /dev/null +++ b/frontend/src/spec/matchers.ts @@ -0,0 +1,34 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +export const customMatchers: jasmine.CustomMatcherFactories = { + toEqualIgnoringProps: (util) => { + return { + compare: (actual: any, expected: any, propsToIgnore: string[] = []) => { + propsToIgnore.push('cachedValues'); + + const omit = (obj: any) => + Object.fromEntries( + Object.entries(obj).filter(([key, value]) => value !== undefined && !propsToIgnore.includes(key)), + ); + + const diffBuilder = new (jasmine as any)['DiffBuilder']({ prettyPrinter: util.pp }); + + const result = {} as jasmine.CustomMatcherResult; + result.pass = (util as any)['equals']( + omit(actual), + omit(expected), + diffBuilder, + ); + + result.message = diffBuilder.getMessage(); + + return result; + }, + }; + }, +}; \ No newline at end of file diff --git a/frontend/tsconfig.spec.json b/frontend/tsconfig.spec.json index 3e64598b6..625226221 100644 --- a/frontend/tsconfig.spec.json +++ b/frontend/tsconfig.spec.json @@ -6,6 +6,7 @@ }, "include": [ "src/**/*.spec.ts", - "src/**/*.d.ts" + "src/**/*.d.ts", + "types/**/*.d.ts", ] } diff --git a/frontend/types/matchers.d.ts b/frontend/types/matchers.d.ts new file mode 100644 index 000000000..f57fe9682 --- /dev/null +++ b/frontend/types/matchers.d.ts @@ -0,0 +1,12 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +declare namespace jasmine { + interface Matchers { + toEqualIgnoringProps(expected: Expected, propsToIgnore?: string[]): boolean; + } +} \ No newline at end of file