diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingCompletion.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingCompletion.cs new file mode 100644 index 000000000..392e381ef --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingCompletion.cs @@ -0,0 +1,104 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using Squidex.Domain.Apps.Core.Schemas; + +namespace Squidex.Domain.Apps.Core.Scripting +{ + public sealed class ScriptingCompletion + { + private readonly Stack prefixes = new Stack(); + private readonly HashSet<(string, string)> result = new HashSet<(string, string)>(); + + public IReadOnlyList<(string Name, string Description)> GetCompletion(Schema schema, PartitionResolver partitionResolver) + { + Push("ctx", "The context object holding all values."); + + Add("appId", "The ID of the current app."); + Add("appName", "The name of the current app."); + Add("contentId", "The ID of the content item."); + Add("operation", "The currnet query operation."); + Add("status", "The status of the content item"); + Add("statusOld", "The old status of the content item."); + + Push("user", "Information about the current user."); + Add("id", "The ID of the user."); + Add("claims", "The additional properties of the user."); + Add("claims.key", "The additional property of the user with name 'key'."); + Add("claims['key']", "The additional property of the user with name 'key'."); + Add("email", "The email address of the current user."); + Add("isClient", "True when the current user is a client."); + Pop(); + + Push("data", "The data of the content item."); + AddData(schema, partitionResolver); + Pop(); + + Push("oldData", "The old data of the content item."); + AddData(schema, partitionResolver); + Pop(); + + Pop(); + + Add("replace()", + "Tell Squidex that you have modified the data and that the change should be applied."); + + Add("disallow()", + "Tell Squidex to not allow the current operation and to return a 403 (Forbidden)."); + + Add("reject('Reason')", + "Tell Squidex to reject the current operation and to return a 403 (Forbidden)."); + + return result.OrderBy(x => x.Item1).ToList(); + } + + private void AddData(Schema schema, PartitionResolver partitionResolver) + { + foreach (var field in schema.Fields.Where(x => x.IsForApi(true))) + { + Push(field.Name, $"The values of the '{field.DisplayName()}' field."); + + foreach (var partition in partitionResolver(field.Partitioning).AllKeys) + { + Push(partition, $"The '{partition}' value of the '{field.DisplayName()}' field."); + + if (field is ArrayField arrayField) + { + foreach (var nestedField in arrayField.Fields.Where(x => x.IsForApi(true))) + { + Push(field.Name, $"The value of the '{nestedField.DisplayName()}' nested field."); + Pop(); + } + } + + Pop(); + } + + Pop(); + } + } + + private void Add(string name, string description) + { + result.Add((string.Join('.', prefixes.Reverse().Union(Enumerable.Repeat(name, 1))), description)); + } + + private void Push(string prefix, string description) + { + Add(prefix, description); + + prefixes.Push(prefix); + } + + private void Pop() + { + prefixes.Pop(); + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs index b6fdf9f7e..2a3c09fe7 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs @@ -6,12 +6,16 @@ // ========================================================================== using System; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; +using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Schemas.Models; +using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Infrastructure; @@ -78,20 +82,9 @@ namespace Squidex.Areas.Api.Controllers.Schemas [ApiCosts(0)] public async Task GetSchema(string app, string name) { - ISchemaEntity? schema; + var schema = await GetSchemaAsync(name); - if (Guid.TryParse(name, out var guid)) - { - var schemaId = DomainId.Create(guid); - - schema = await appProvider.GetSchemaAsync(AppId, schemaId); - } - else - { - schema = await appProvider.GetSchemaAsync(AppId, name); - } - - if (schema == null || schema.IsDeleted) + if (schema == null) { return NotFound(); } @@ -346,6 +339,42 @@ namespace Squidex.Areas.Api.Controllers.Schemas return NoContent(); } + [HttpGet] + [Route("apps/{app}/schemas/{name}/completion")] + [ApiPermissionOrAnonymous] + [ApiCosts(1)] + [OpenApiIgnore] + public async Task GetScriptCompletiong(string app, string name) + { + var schema = await GetSchemaAsync(name); + + if (schema == null) + { + return NotFound(); + } + + var completer = new ScriptingCompletion(); + var completion = completer.GetCompletion(schema.SchemaDef, App.PartitionResolver()); + + var result = completion.Select(x => new { x.Name, x.Description }); + + return Ok(result); + } + + private Task GetSchemaAsync(string name) + { + if (Guid.TryParse(name, out var guid)) + { + var schemaId = DomainId.Create(guid); + + return appProvider.GetSchemaAsync(AppId, schemaId); + } + else + { + return appProvider.GetSchemaAsync(AppId, name); + } + } + private async Task InvokeCommandAsync(ICommand command) { var context = await CommandBus.PublishAsync(command); diff --git a/frontend/app-config/webpack.config.js b/frontend/app-config/webpack.config.js index 2b8afac44..62099a8ae 100644 --- a/frontend/app-config/webpack.config.js +++ b/frontend/app-config/webpack.config.js @@ -225,7 +225,9 @@ module.exports = function (env) { { from: './node_modules/ace-builds/src-min/ace.js', to: 'dependencies/ace/ace.js' }, { from: './node_modules/ace-builds/src-min/mode-*.js', to: 'dependencies/ace/[name].[ext]' }, { from: './node_modules/ace-builds/src-min/worker-*.js', to: 'dependencies/ace/[name].[ext]' }, + { from: './node_modules/ace-builds/src-min/snippets', to: 'dependencies/ace/snippets' }, { from: './node_modules/ace-builds/src-min/ext-modelist.js', to: 'dependencies/ace/ext/modelist.js' }, + { from: './node_modules/ace-builds/src-min/ext-language_tools.js', to: 'dependencies/ace/ext/language_tools.js' }, { from: './node_modules/video.js/dist/video.min.js', to: 'dependencies/videojs' }, { from: './node_modules/video.js/dist/video-js.min.css', to: 'dependencies/videojs' }, diff --git a/frontend/app/features/schemas/pages/schema/scripts/schema-scripts-form.component.html b/frontend/app/features/schemas/pages/schema/scripts/schema-scripts-form.component.html index 1b439b5b8..ed1b18fc8 100644 --- a/frontend/app/features/schemas/pages/schema/scripts/schema-scripts-form.component.html +++ b/frontend/app/features/schemas/pages/schema/scripts/schema-scripts-form.component.html @@ -2,7 +2,7 @@