2026-09-25 06:39:34 +00:00
|
|
|
|
using System;
|
2026-09-18 13:47:23 +00:00
|
|
|
|
using System.Collections.Generic;
|
2026-09-25 07:52:06 +00:00
|
|
|
|
using System.Data;
|
2026-09-24 13:29:53 +00:00
|
|
|
|
using System.Globalization;
|
2026-09-18 13:47:23 +00:00
|
|
|
|
using System.Linq;
|
|
|
|
|
|
using System.Text.Json;
|
|
|
|
|
|
using System.Text.Json.Nodes;
|
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
|
using Microsoft.AspNetCore.Authorization;
|
2026-09-25 07:52:06 +00:00
|
|
|
|
using Microsoft.Extensions.Configuration;
|
2026-09-18 13:47:23 +00:00
|
|
|
|
using Sozsoft.Mcp.Dtos;
|
|
|
|
|
|
using Sozsoft.Mcp.Services;
|
|
|
|
|
|
using Sozsoft.Platform;
|
2026-09-25 07:52:06 +00:00
|
|
|
|
using Sozsoft.Platform.Data.Seeds;
|
2026-09-18 13:47:23 +00:00
|
|
|
|
using Sozsoft.Platform.DeveloperKit;
|
2026-09-24 11:58:23 +00:00
|
|
|
|
using Sozsoft.Platform.Entities;
|
|
|
|
|
|
using Sozsoft.Platform.Enums;
|
2026-09-18 13:47:23 +00:00
|
|
|
|
using Sozsoft.Platform.ListForms;
|
2026-09-24 11:58:23 +00:00
|
|
|
|
using Sozsoft.Platform.ListForms.Select;
|
2026-09-18 13:47:23 +00:00
|
|
|
|
using Volo.Abp;
|
|
|
|
|
|
using Volo.Abp.Application.Dtos;
|
2026-09-24 11:58:23 +00:00
|
|
|
|
using Volo.Abp.Domain.Repositories;
|
|
|
|
|
|
using Volo.Abp.PermissionManagement;
|
2026-09-18 13:47:23 +00:00
|
|
|
|
|
|
|
|
|
|
namespace Sozsoft.Mcp;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Ekran ve bilesen gelistirme yuzeyi. Uretim Wizard ve Custom Components servislerinden
|
|
|
|
|
|
/// gecer: menu, yetki, dil anahtari, seed dosyasi ve bagimlilik kurallari ekrandaki yolla
|
2026-09-24 11:58:23 +00:00
|
|
|
|
/// aynidir, yetki kontrolu de o servislerin kendi politikalarindan gelir. Invoke/Write yetkisi
|
2026-09-18 13:47:23 +00:00
|
|
|
|
/// yalnizca MCP kapisidir, arkadaki yetkinin yerine gecmez.
|
|
|
|
|
|
/// </summary>
|
2026-09-24 11:58:23 +00:00
|
|
|
|
[Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Invoke)]
|
2026-09-18 13:47:23 +00:00
|
|
|
|
public class McpDevelopAppService(
|
|
|
|
|
|
IMcpServerConfigProvider serverConfigProvider,
|
|
|
|
|
|
IMcpDevGuide devGuide,
|
|
|
|
|
|
IMcpTableCreator tableCreator,
|
2026-10-07 18:46:45 +00:00
|
|
|
|
IMcpSqlObjectWriter sqlObjectWriter,
|
2026-09-18 13:47:23 +00:00
|
|
|
|
IListFormWizardAppService wizardAppService,
|
2026-09-24 11:58:23 +00:00
|
|
|
|
ICustomComponentAppService componentAppService,
|
|
|
|
|
|
IListFormSelectAppService listFormSelectAppService,
|
|
|
|
|
|
IRepository<Menu, Guid> menuRepository,
|
|
|
|
|
|
IRepository<PermissionDefinitionRecord, Guid> permissionRepository,
|
2026-09-25 07:52:06 +00:00
|
|
|
|
IRepository<DevRequest, Guid> devRequestRepository,
|
2026-09-28 12:50:38 +00:00
|
|
|
|
IMcpDevRequestTimesheetRecorder timesheetRecorder,
|
2026-10-04 15:04:16 +00:00
|
|
|
|
IModuleDocAppService moduleDocAppService,
|
2026-09-25 07:52:06 +00:00
|
|
|
|
IConfiguration configuration)
|
2026-09-18 13:47:23 +00:00
|
|
|
|
: McpAppService(serverConfigProvider), IMcpDevelopAppService
|
|
|
|
|
|
{
|
|
|
|
|
|
private const string DefaultGuideDocument = "lowcode";
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>Index cevabinda listelenen en derin baslik; daha derini bolum istenince gorunur.</summary>
|
|
|
|
|
|
private const int IndexHeadingLevel = 3;
|
|
|
|
|
|
|
2026-10-02 06:07:01 +00:00
|
|
|
|
/// <summary>Baslikla eslesmeyen terim icin listelenen en fazla bolum.</summary>
|
|
|
|
|
|
private const int GuideSearchMaxResults = 15;
|
|
|
|
|
|
|
2026-09-18 13:47:23 +00:00
|
|
|
|
/// <summary>Custom Components ekraninin tek sayfada dondurebildigi ust sinir.</summary>
|
|
|
|
|
|
private const int ComponentPageSize = 1000;
|
|
|
|
|
|
|
2026-09-24 11:58:23 +00:00
|
|
|
|
private const string DevRequestUpdatePermission = PlatformConsts.AppCodes.DevRequests + ".Update";
|
|
|
|
|
|
|
2026-10-04 15:04:16 +00:00
|
|
|
|
/// <summary>Liste ekrani aciklamasinin (DescTr/DescEn) en az satir ve karakter sayisi.</summary>
|
|
|
|
|
|
private const int MinDescriptionLines = 3;
|
|
|
|
|
|
private const int MinDescriptionLength = 150;
|
|
|
|
|
|
|
2026-09-24 13:29:53 +00:00
|
|
|
|
/// <summary>Talep ekranindaki alt gorev tarih kutusunun okudugu bicim.</summary>
|
|
|
|
|
|
private const string SubTaskDateFormat = "yyyy-MM-dd";
|
|
|
|
|
|
|
2026-09-24 11:58:23 +00:00
|
|
|
|
/// <summary>verify_wizard cevabindaki kontrol adlari.</summary>
|
|
|
|
|
|
private static class VerifyChecks
|
|
|
|
|
|
{
|
|
|
|
|
|
public const string WizardFile = "wizard_file";
|
2026-09-25 06:39:34 +00:00
|
|
|
|
public const string Install = "install";
|
2026-09-24 11:58:23 +00:00
|
|
|
|
public const string Menu = "menu";
|
|
|
|
|
|
public const string Permission = "permission";
|
|
|
|
|
|
public const string Select = "select";
|
|
|
|
|
|
public const string Component = "component";
|
|
|
|
|
|
public const string SeedClosure = "seed_closure";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
public async Task<McpOutcome<McpDevGuideDto>> GetDevGuideAsync(string? document, string? section)
|
2026-09-18 13:47:23 +00:00
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (await EnsureToolAllowedAsync(McpToolNames.GetDevGuide) is { Problem: { } problem })
|
|
|
|
|
|
{
|
|
|
|
|
|
return problem;
|
|
|
|
|
|
}
|
2026-09-18 13:47:23 +00:00
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(section))
|
|
|
|
|
|
{
|
|
|
|
|
|
var headings = await devGuide.GetHeadingsAsync();
|
|
|
|
|
|
if (headings.Count == 0)
|
|
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
return Problem(McpErrorCodes.DevGuideUnavailable, McpDevGuide.ConfigurationKey);
|
2026-09-18 13:47:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return new McpDevGuideDto
|
|
|
|
|
|
{
|
|
|
|
|
|
Sections = [.. headings
|
|
|
|
|
|
.Where(h => h.Level <= IndexHeadingLevel)
|
|
|
|
|
|
.Where(h => string.IsNullOrWhiteSpace(document) || string.Equals(h.Document, document, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
|
.Select(ToDto)]
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-02 06:07:01 +00:00
|
|
|
|
var result = await FindGuideSectionAsync(document, section);
|
|
|
|
|
|
if (result is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Baslik olmayan bir terim (editor, alan adi) istenebilir; hata yerine terimin gectigi
|
|
|
|
|
|
// bolumler, o da yoksa baslik listesi doner ki model bir sonraki istegi secebilsin.
|
|
|
|
|
|
var matches = await devGuide.SearchAsync(section, GuideSearchMaxResults);
|
|
|
|
|
|
if (matches.Count > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
return new McpDevGuideDto
|
|
|
|
|
|
{
|
|
|
|
|
|
Content = L[McpErrorCodes.DevGuideSearchMatches, section],
|
|
|
|
|
|
Sections = [.. matches.Select(ToDto)]
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var headings = await devGuide.GetHeadingsAsync();
|
|
|
|
|
|
return new McpDevGuideDto
|
|
|
|
|
|
{
|
|
|
|
|
|
Content = L[McpErrorCodes.DevGuideSectionNotFound, section],
|
|
|
|
|
|
Sections = [.. headings.Where(h => h.Level <= IndexHeadingLevel).Select(ToDto)]
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2026-09-18 13:47:23 +00:00
|
|
|
|
|
|
|
|
|
|
return new McpDevGuideDto
|
|
|
|
|
|
{
|
|
|
|
|
|
Heading = ToDto(result.Heading),
|
|
|
|
|
|
Content = result.Content,
|
|
|
|
|
|
Sections = [.. result.Children.Select(ToDto)]
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-02 06:07:01 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Bolum once istenen belgede, sonra diger belgelerde numara ya da baslik metniyle aranir;
|
|
|
|
|
|
/// model terimi hangi belgede oldugunu bilmeden isteyebilir.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async Task<McpDevGuideSection?> FindGuideSectionAsync(string? document, string section)
|
|
|
|
|
|
{
|
|
|
|
|
|
var first = string.IsNullOrWhiteSpace(document) ? DefaultGuideDocument : document;
|
|
|
|
|
|
foreach (var candidate in McpDevGuide.Documents.Keys.OrderBy(key => !string.Equals(key, first, StringComparison.OrdinalIgnoreCase)))
|
|
|
|
|
|
{
|
|
|
|
|
|
var result = await devGuide.GetSectionAsync(candidate, section);
|
|
|
|
|
|
if (result is not null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:47:23 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Tablo tasarimcisiyla ayni yetkiyi ister: ham DDL liste yetkilerini asar, Develop yalnizca
|
|
|
|
|
|
/// MCP kapisidir.
|
|
|
|
|
|
/// </summary>
|
2026-10-10 22:03:36 +00:00
|
|
|
|
public async Task<McpOutcome<McpCreateTableResultDto>> CreateTableAsync(McpCreateTableRequestDto input)
|
2026-09-18 13:47:23 +00:00
|
|
|
|
{
|
|
|
|
|
|
Check.NotNull(input, nameof(input));
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (await EnsureToolAllowedAsync(McpToolNames.CreateTable) is { Problem: { } problem })
|
|
|
|
|
|
{
|
|
|
|
|
|
return problem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (await CheckPermissionAsync(McpToolNames.CreateTable, PlatformConsts.AppCodes.SqlQueryManagers.Default) is { } denied)
|
|
|
|
|
|
{
|
|
|
|
|
|
return denied;
|
|
|
|
|
|
}
|
2026-09-18 13:47:23 +00:00
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
var outcome = await tableCreator.CreateAsync(new McpTableDefinition(
|
2026-09-18 13:47:23 +00:00
|
|
|
|
input.TableName,
|
|
|
|
|
|
[.. (input.Columns ?? []).Select(column => new McpTableColumn(
|
|
|
|
|
|
column.Name, column.Type, column.MaxLength, column.IsNullable, column.DefaultValue))],
|
|
|
|
|
|
input.FullAudited,
|
2026-10-07 18:46:45 +00:00
|
|
|
|
input.MultiTenant,
|
|
|
|
|
|
[.. (input.Indexes ?? []).Select(index => new McpTableIndex(index.Columns ?? [], index.Unique))],
|
|
|
|
|
|
[.. (input.ForeignKeys ?? []).Select(fk => new McpTableForeignKey(fk.Column, fk.ReferencedTable, fk.ReferencedColumn, fk.OnDelete))]));
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (!outcome.TryGetValue(out var created, out var invalid))
|
|
|
|
|
|
{
|
|
|
|
|
|
return invalid;
|
|
|
|
|
|
}
|
2026-09-18 13:47:23 +00:00
|
|
|
|
|
2026-09-28 12:50:38 +00:00
|
|
|
|
await timesheetRecorder.RecordAsync(McpTimesheetTexts.CreateTable, created.TableName);
|
|
|
|
|
|
|
2026-09-18 13:47:23 +00:00
|
|
|
|
return new McpCreateTableResultDto
|
|
|
|
|
|
{
|
|
|
|
|
|
TableName = created.TableName,
|
|
|
|
|
|
Script = created.Script,
|
|
|
|
|
|
SeedFilePath = created.SeedFilePath
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-07 18:46:45 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Tablo tasarimcisiyla ayni yetkiyi ister: SQL Query Manager'daki view/procedure/function
|
|
|
|
|
|
/// kaydinin karsiligidir. Nesneyi DB Migrate kurar.
|
|
|
|
|
|
/// </summary>
|
2026-10-10 22:03:36 +00:00
|
|
|
|
public async Task<McpOutcome<McpSqlObjectResultDto>> SaveSqlObjectAsync(string sqlServerScript, string postgreSqlScript)
|
2026-10-07 18:46:45 +00:00
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (await EnsureToolAllowedAsync(McpToolNames.SaveSqlObject) is { Problem: { } problem })
|
|
|
|
|
|
{
|
|
|
|
|
|
return problem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (await CheckPermissionAsync(McpToolNames.SaveSqlObject, PlatformConsts.AppCodes.SqlQueryManagers.Default) is { } denied)
|
|
|
|
|
|
{
|
|
|
|
|
|
return denied;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!sqlObjectWriter.Save(sqlServerScript, postgreSqlScript).TryGetValue(out var saved, out var invalid))
|
|
|
|
|
|
{
|
|
|
|
|
|
return invalid;
|
|
|
|
|
|
}
|
2026-10-07 18:46:45 +00:00
|
|
|
|
|
|
|
|
|
|
await timesheetRecorder.RecordAsync(McpTimesheetTexts.SaveSqlObject, saved.Name);
|
|
|
|
|
|
|
|
|
|
|
|
return new McpSqlObjectResultDto
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = saved.Name,
|
|
|
|
|
|
Kind = saved.Kind,
|
|
|
|
|
|
SeedFilePaths = [saved.SqlServerPath, saved.PostgreSqlPath]
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// SQL Query Manager'daki CRUD endpoint diyaloguyla ayni endpoint kumesini
|
|
|
|
|
|
/// (<see cref="CrudEndpointSeedFileDto.Generate"/>) yalnizca <c>crud/{Entity}.json</c> seed dosyasina
|
|
|
|
|
|
/// yazar; kayitlari DB Migrate ekler (<c>CrudEndpointSeedApplier</c>). Yetki diyalogunkiyle aynidir.
|
|
|
|
|
|
/// </summary>
|
2026-10-10 22:03:36 +00:00
|
|
|
|
public async Task<McpOutcome<McpCrudEndpointsResultDto>> GenerateCrudEndpointsAsync(string entityName, string? operationTypes, string? objectType)
|
2026-10-07 18:46:45 +00:00
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (await EnsureToolAllowedAsync(McpToolNames.GenerateCrudEndpoints) is { Problem: { } problem })
|
|
|
|
|
|
{
|
|
|
|
|
|
return problem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (await CheckPermissionAsync(McpToolNames.GenerateCrudEndpoints, PlatformConsts.AppCodes.SqlQueryManagers.CrudEndpoints) is { } denied)
|
|
|
|
|
|
{
|
|
|
|
|
|
return denied;
|
|
|
|
|
|
}
|
2026-10-07 18:46:45 +00:00
|
|
|
|
|
|
|
|
|
|
// Tasarimcidaki gibi sema oneki atilir: endpoint yolu yalnizca nesne adini tasir.
|
|
|
|
|
|
var entity = entityName?.Split('.')[^1].Trim('[', ']', '"', ' ') ?? string.Empty;
|
|
|
|
|
|
if (CrudEndpointSeedFileDto.SafeFileName(entity) is null)
|
|
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
return Problem(McpErrorCodes.InvalidIdentifier, entityName ?? string.Empty);
|
2026-10-07 18:46:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-17 22:31:09 +00:00
|
|
|
|
// Model listeyi JSON dizisi olarak da gonderebilir (["GetList","Create"]); metne cevrilen dizi
|
|
|
|
|
|
// virgullu listeye indirilir.
|
|
|
|
|
|
var operations = operationTypes is null
|
|
|
|
|
|
? null
|
|
|
|
|
|
: string.Join(',', operationTypes.Split(',').Select(item => item.Trim('[', ']', '"', ' ')));
|
|
|
|
|
|
var seedFile = CrudEndpointSeedFileDto.Generate(entity, operations, objectType, Clock.Now.ToUniversalTime());
|
2026-10-07 18:46:45 +00:00
|
|
|
|
seedFile.Write(configuration);
|
|
|
|
|
|
await timesheetRecorder.RecordAsync(McpTimesheetTexts.GenerateCrudEndpoints, entity);
|
|
|
|
|
|
|
|
|
|
|
|
return new McpCrudEndpointsResultDto
|
|
|
|
|
|
{
|
|
|
|
|
|
EntityName = entity,
|
|
|
|
|
|
SeedFilePath = $"{SeedPathResolver.CrudFolder}/{CrudEndpointSeedFileDto.SafeFileName(entity)}.json",
|
|
|
|
|
|
Endpoints = [.. seedFile.Endpoints.Select(e => $"{e.Method} {e.Path}{(e.IsActive ? string.Empty : " [inactive]")}")]
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
public async Task<McpOutcome<List<WizardFileInfoDto>>> GetWizardsAsync(string? search)
|
2026-09-18 13:47:23 +00:00
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (await EnsureToolAllowedAsync(McpToolNames.ListWizards) is { Problem: { } problem })
|
|
|
|
|
|
{
|
|
|
|
|
|
return problem;
|
|
|
|
|
|
}
|
2026-09-18 13:47:23 +00:00
|
|
|
|
|
|
|
|
|
|
var files = await wizardAppService.GetFiles();
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(search))
|
|
|
|
|
|
{
|
|
|
|
|
|
return files;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
return files.Where(file =>
|
2026-09-18 13:47:23 +00:00
|
|
|
|
Contains(file.FileName, search) || Contains(file.WizardName, search) ||
|
2026-10-10 22:03:36 +00:00
|
|
|
|
Contains(file.ListFormCode, search) || Contains(file.CustomComponentName, search)).ToList();
|
2026-09-18 13:47:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
public async Task<McpOutcome<McpWizardFileDto>> GetWizardAsync(string fileName)
|
2026-09-18 13:47:23 +00:00
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (await EnsureToolAllowedAsync(McpToolNames.GetWizard) is { Problem: { } problem })
|
|
|
|
|
|
{
|
|
|
|
|
|
return problem;
|
|
|
|
|
|
}
|
2026-09-18 13:47:23 +00:00
|
|
|
|
|
|
|
|
|
|
var seed = await wizardAppService.GetFile(fileName);
|
|
|
|
|
|
|
|
|
|
|
|
// Model dosyayi Claude Code'un depoda gordugu bicimde gorur; deploy_wizard ayni bicimi
|
|
|
|
|
|
// geri kabul eder.
|
|
|
|
|
|
using var document = JsonDocument.Parse(seed.ToSeedJson());
|
|
|
|
|
|
|
|
|
|
|
|
return new McpWizardFileDto { FileName = fileName, Seed = document.RootElement.Clone() };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
public async Task<McpOutcome<WizardFileInfoDto?>> DeployWizardAsync(McpWizardDeployRequestDto input)
|
2026-09-18 13:47:23 +00:00
|
|
|
|
{
|
|
|
|
|
|
Check.NotNull(input, nameof(input));
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (await EnsureToolAllowedAsync(McpToolNames.DeployWizard) is { Problem: { } problem })
|
|
|
|
|
|
{
|
|
|
|
|
|
return problem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!ParseWizard(input.Wizard).TryGetValue(out var wizard, out var invalid))
|
|
|
|
|
|
{
|
|
|
|
|
|
return invalid;
|
|
|
|
|
|
}
|
2026-09-18 13:47:23 +00:00
|
|
|
|
|
|
|
|
|
|
wizard.EditFileName = string.IsNullOrWhiteSpace(input.ReplaceFileName) ? null : input.ReplaceFileName.Trim();
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (CheckDetailedDescription(wizard) is { } shortDescription)
|
|
|
|
|
|
{
|
|
|
|
|
|
return shortDescription;
|
|
|
|
|
|
}
|
2026-09-18 13:47:23 +00:00
|
|
|
|
|
2026-09-28 12:50:38 +00:00
|
|
|
|
// Menu sirasi modelin karari degildir: yeni ekran kardeslerinin sonuna eklenir (deploy
|
|
|
|
|
|
// bagimlilik sirasiyla yapildigi icin lookup kaynagi tuketen ekranin ustunde kalir),
|
|
|
|
|
|
// guncelleme ekranin mevcut yerini korur.
|
2026-10-02 09:29:22 +00:00
|
|
|
|
var previous = wizard.EditFileName is null ? null : (await wizardAppService.GetFile(wizard.EditFileName)).Wizard;
|
|
|
|
|
|
wizard.MenuOrder = previous?.MenuOrder ?? 0;
|
|
|
|
|
|
|
2026-10-17 22:31:09 +00:00
|
|
|
|
// deploy_wizard dosyayi tamamen degistirir. Duzenlemede WizardCode yazilmamissa kod sessizce
|
|
|
|
|
|
// App.Wizard.{Ad}'a duser: girdi kismidir ve deploy ekranin menusunu, yetkilerini ve alanlarini
|
|
|
|
|
|
// bosaltirdi. Kodu bilerek degistiren girdi WizardCode'u acikca yazar.
|
|
|
|
|
|
if (previous is not null && string.IsNullOrWhiteSpace(wizard.ListFormCode) &&
|
|
|
|
|
|
!string.IsNullOrWhiteSpace(previous.ListFormCode) &&
|
|
|
|
|
|
!string.Equals(previous.ListFormCode.Trim(), wizard.ResolveListFormCode(), StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
|
{
|
|
|
|
|
|
return Problem(McpErrorCodes.WizardCodeMissing, wizard.EditFileName!, previous.ListFormCode.Trim());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-02 09:29:22 +00:00
|
|
|
|
// Alt form sekmesi ana kayit her degistiginde yeniden sorgulanmaz; MCP yeni baglanti
|
|
|
|
|
|
// acarken yenilemeyi kapali yazar. Var olan baglantida kullanicinin ekrandan verdigi
|
|
|
|
|
|
// deger korunur.
|
|
|
|
|
|
var previousRefresh = (previous?.SubForms ?? [])
|
|
|
|
|
|
.Where(subForm => !string.IsNullOrWhiteSpace(subForm.Code))
|
|
|
|
|
|
.GroupBy(subForm => subForm.Code, StringComparer.OrdinalIgnoreCase)
|
|
|
|
|
|
.ToDictionary(group => group.Key, group => group.First().IsRefresh, StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
|
foreach (var subForm in wizard.SubForms ?? [])
|
|
|
|
|
|
subForm.IsRefresh = subForm.Code is not null && previousRefresh.GetValueOrDefault(subForm.Code);
|
2026-09-28 12:50:38 +00:00
|
|
|
|
|
2026-09-30 14:57:25 +00:00
|
|
|
|
// Tek gruplu duzenleme formunda grup basligi popup basligini tekrarlar; baslik yalnizca
|
|
|
|
|
|
// birden fazla grubu birbirinden ayirmak icin yazilir.
|
|
|
|
|
|
var editGroups = wizard.Groups
|
|
|
|
|
|
.Where(group => group.Items.Any(item => item.IncludeInEditingForm && item.FieldName != wizard.KeyFieldName))
|
|
|
|
|
|
.ToList();
|
|
|
|
|
|
if (editGroups.Count == 1)
|
|
|
|
|
|
editGroups[0].Caption = string.Empty;
|
|
|
|
|
|
|
2026-09-25 07:52:06 +00:00
|
|
|
|
// Satirlar deploy'dan once dogrulanir; gecersiz girdi yarim bir deploy birakmaz.
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (!(await ParseSeedRowsAsync(input.SeedRows, wizard)).TryGetValue(out var seedRows, out invalid))
|
|
|
|
|
|
{
|
|
|
|
|
|
return invalid;
|
|
|
|
|
|
}
|
2026-09-25 07:52:06 +00:00
|
|
|
|
|
2026-09-30 07:28:12 +00:00
|
|
|
|
// Wizard dosyasi onu ureten talebi tasir; Wizard Manager talebi gosterir ve talebin
|
|
|
|
|
|
// butun wizard'larini birlikte export eder. Bag modelin girdisinden degil etkin talepten gelir.
|
|
|
|
|
|
wizard.DevRequestId = await timesheetRecorder.GetActiveRequestIdAsync();
|
|
|
|
|
|
|
2026-09-18 13:47:23 +00:00
|
|
|
|
await wizardAppService.Create(wizard);
|
|
|
|
|
|
|
|
|
|
|
|
var files = await wizardAppService.GetFiles();
|
2026-09-25 07:52:06 +00:00
|
|
|
|
var file = files.FirstOrDefault(file => string.Equals(file.WizardName, wizard.WizardName?.Trim(), StringComparison.OrdinalIgnoreCase));
|
|
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (file != null && seedRows.Count > 0 && await WriteSeedRowsAsync(file.FileName, seedRows) is { } rowsRejected)
|
|
|
|
|
|
return rowsRejected;
|
2026-09-18 13:47:23 +00:00
|
|
|
|
|
2026-09-28 12:50:38 +00:00
|
|
|
|
await timesheetRecorder.RecordAsync(McpTimesheetTexts.DeployWizard, file?.FileName ?? wizard.WizardName, seedRows.Count);
|
|
|
|
|
|
|
2026-09-25 07:52:06 +00:00
|
|
|
|
return file;
|
2026-09-18 13:47:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-04 15:04:16 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Liste ekraninin aciklamasi basliktaki bilgi simgesinde (ⓘ) gosterilir ve ekranin kullanim
|
|
|
|
|
|
/// kilavuzudur: ne ise yaradigi, menude/hangi ekranin sekmesinde durdugu, ana alanlari, otomatik
|
|
|
|
|
|
/// hesaplar ve iliskili ekranlar (lowcode §4.4.2). Tek satirlik aciklama bu ihtiyaci karsilamadigi
|
|
|
|
|
|
/// icin MCP deploy'u kisa aciklamayi reddeder.
|
|
|
|
|
|
/// </summary>
|
2026-10-10 22:03:36 +00:00
|
|
|
|
private McpProblem? CheckDetailedDescription(ListFormWizardDto wizard)
|
2026-10-04 15:04:16 +00:00
|
|
|
|
{
|
|
|
|
|
|
if (wizard.ComponentKind == WizardComponentKindEnum.Custom)
|
2026-10-10 22:03:36 +00:00
|
|
|
|
return null;
|
2026-10-04 15:04:16 +00:00
|
|
|
|
|
|
|
|
|
|
static bool IsDetailed(string? text) =>
|
|
|
|
|
|
!string.IsNullOrWhiteSpace(text) &&
|
|
|
|
|
|
text.Trim().Length >= MinDescriptionLength &&
|
|
|
|
|
|
text.Split('\n').Count(line => !string.IsNullOrWhiteSpace(line)) >= MinDescriptionLines;
|
|
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
return IsDetailed(wizard.DescTr) && IsDetailed(wizard.DescEn)
|
|
|
|
|
|
? null
|
|
|
|
|
|
: Problem(McpErrorCodes.WizardDescriptionTooShort, MinDescriptionLines, MinDescriptionLength);
|
2026-10-04 15:04:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-25 07:52:06 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Demo satirlarini okur ve ekranin alanlarina gore dogrular. Satirlar yalnizca SeedData acik
|
|
|
|
|
|
/// liste ekraninda kabul edilir: migrate onlari ekranin veri dosyasindan ekler.
|
|
|
|
|
|
/// </summary>
|
2026-10-10 22:03:36 +00:00
|
|
|
|
private async Task<McpOutcome<List<Dictionary<string, JsonElement>>>> ParseSeedRowsAsync(string? json, ListFormWizardDto wizard)
|
2026-09-25 07:52:06 +00:00
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(json))
|
2026-10-10 22:03:36 +00:00
|
|
|
|
return new List<Dictionary<string, JsonElement>>();
|
2026-09-25 07:52:06 +00:00
|
|
|
|
|
|
|
|
|
|
if (wizard.ComponentKind == WizardComponentKindEnum.Custom || !wizard.SeedData)
|
2026-10-10 22:03:36 +00:00
|
|
|
|
return Problem(McpErrorCodes.SeedRowsNotAllowed);
|
2026-09-25 07:52:06 +00:00
|
|
|
|
|
|
|
|
|
|
List<Dictionary<string, JsonElement>>? rows;
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
rows = JsonSerializer.Deserialize<List<Dictionary<string, JsonElement>>>(json);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (JsonException)
|
|
|
|
|
|
{
|
|
|
|
|
|
rows = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (rows is null || rows.Any(row => row is null))
|
2026-10-10 22:03:36 +00:00
|
|
|
|
return Problem(McpErrorCodes.SeedRowsInvalid);
|
2026-09-25 07:52:06 +00:00
|
|
|
|
|
|
|
|
|
|
var fields = wizard.Groups.SelectMany(group => group.Items).Select(item => item.FieldName)
|
|
|
|
|
|
.Append(wizard.KeyFieldName)
|
|
|
|
|
|
.Where(name => !string.IsNullOrWhiteSpace(name))
|
|
|
|
|
|
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
|
|
|
|
|
|
|
var unknown = rows.SelectMany(row => row.Keys).Where(key => !fields.Contains(key))
|
|
|
|
|
|
.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
2026-10-07 14:34:49 +00:00
|
|
|
|
if (unknown.Count == 0)
|
|
|
|
|
|
return rows;
|
|
|
|
|
|
|
|
|
|
|
|
var screenFields = string.Join(", ", fields);
|
|
|
|
|
|
var owners = await FindSameTableScreensAsync(wizard, unknown);
|
|
|
|
|
|
if (owners.Count == 0)
|
2026-10-10 22:03:36 +00:00
|
|
|
|
return Problem(McpErrorCodes.SeedRowsUnknownFields, string.Join(", ", unknown), screenFields);
|
2026-10-07 14:34:49 +00:00
|
|
|
|
|
|
|
|
|
|
// Ayni tablonun baska ekraninda (cogunlukla alt form sekmesi) alan olan kolon migrate'te
|
|
|
|
|
|
// bu ekranin veri dosyasindan yazilmaz; model kolonun nerede durdugunu gorurse alani ekrana
|
|
|
|
|
|
// ekleyip ya da satirdan cikarip tekrar dener.
|
|
|
|
|
|
var placement = string.Join("; ", owners.Select(owner => $"{owner.Key}: {string.Join(", ", owner.Value)}"));
|
2026-10-10 22:03:36 +00:00
|
|
|
|
return Problem(McpErrorCodes.SeedRowsFieldsOnOtherScreens, string.Join(", ", unknown), placement, screenFields);
|
2026-10-07 14:34:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Bilinmeyen kolonlari ayni tabloyu okuyan diger wizard ekranlarinin alanlarinda arar; sonuc
|
|
|
|
|
|
/// ekran kodundan o ekranda bulunan kolonlara eslemedir. Ekran listesi wizard dosyalarindan
|
|
|
|
|
|
/// okunur, migrate bekleyen ekranlar da dahildir.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async Task<Dictionary<string, List<string>>> FindSameTableScreensAsync(ListFormWizardDto wizard, List<string> columns)
|
|
|
|
|
|
{
|
|
|
|
|
|
var owners = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
|
var table = wizard.SelectCommand?.Trim();
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(table))
|
|
|
|
|
|
return owners;
|
|
|
|
|
|
|
|
|
|
|
|
var wizardName = wizard.WizardName?.Trim();
|
|
|
|
|
|
foreach (var file in await wizardAppService.GetFiles())
|
|
|
|
|
|
{
|
|
|
|
|
|
if (file.ComponentKind == WizardComponentKindEnum.Custom ||
|
|
|
|
|
|
string.Equals(file.WizardName, wizardName, StringComparison.OrdinalIgnoreCase) ||
|
|
|
|
|
|
string.Equals(file.FileName, wizard.EditFileName, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
|
|
var seed = await wizardAppService.GetFile(file.FileName);
|
|
|
|
|
|
if (!string.Equals(seed.Wizard?.SelectCommand?.Trim(), table, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
|
|
var found = columns
|
|
|
|
|
|
.Where(column => (seed.Fields ?? []).Any(field => string.Equals(field.FieldName, column, StringComparison.OrdinalIgnoreCase)))
|
|
|
|
|
|
.ToList();
|
|
|
|
|
|
if (found.Count > 0)
|
|
|
|
|
|
owners[seed.ListForm?.ListFormCode ?? file.ListFormCode ?? file.WizardName] = found;
|
|
|
|
|
|
}
|
2026-09-25 07:52:06 +00:00
|
|
|
|
|
2026-10-07 14:34:49 +00:00
|
|
|
|
return owners;
|
2026-09-25 07:52:06 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-10-07 14:48:16 +00:00
|
|
|
|
/// Demo satirlarini ekranin veri seed hedefine (<c>ListForm.SeedFilePath</c>; tanima gore tek
|
|
|
|
|
|
/// dosya ya da satir klasoru) yazar; veritabanina dokunmaz. Migrate ekrani kurduktan sonra
|
|
|
|
|
|
/// <c>ListFormSeedDataApplier</c> satirlari ekler ve kiraci/audit alanlarini doldurur. Guid anahtar
|
|
|
|
|
|
/// burada uretilir: dosya her uygulandiginda ayni kayda karsilik gelir. Hedefte ayni icerikli satir
|
|
|
|
|
|
/// varsa (tekrar deploy) yeniden eklenmez.
|
2026-09-25 07:52:06 +00:00
|
|
|
|
/// </summary>
|
2026-10-10 22:03:36 +00:00
|
|
|
|
private async Task<McpProblem?> WriteSeedRowsAsync(string fileName, List<Dictionary<string, JsonElement>> rows)
|
2026-09-25 07:52:06 +00:00
|
|
|
|
{
|
|
|
|
|
|
var listForm = (await wizardAppService.GetFile(fileName)).ListForm;
|
2026-10-07 14:48:16 +00:00
|
|
|
|
var target = ListFormSeedTarget.Create(SeedPathResolver.GetRootPath(configuration), listForm?.SeedFilePath);
|
|
|
|
|
|
if (listForm == null || target == null)
|
2026-10-10 22:03:36 +00:00
|
|
|
|
return Problem(McpErrorCodes.SeedRowsNotAllowed);
|
2026-09-25 07:52:06 +00:00
|
|
|
|
|
2026-10-07 14:48:16 +00:00
|
|
|
|
var keyFieldName = listForm.KeyFieldName;
|
|
|
|
|
|
var existing = (await target.ReadRowsAsync()).Select(row => RowContent(row, keyFieldName)).ToHashSet(StringComparer.Ordinal);
|
|
|
|
|
|
var added = new List<(object Key, Dictionary<string, object> Row)>();
|
2026-09-25 07:52:06 +00:00
|
|
|
|
|
|
|
|
|
|
foreach (var row in rows)
|
|
|
|
|
|
{
|
|
|
|
|
|
var values = row.ToDictionary(item => item.Key, item => (object)item.Value);
|
|
|
|
|
|
if (!existing.Add(RowContent(values, keyFieldName)))
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
|
|
if (listForm.KeyFieldDbSourceType == DbType.Guid && !values.Keys.Contains(keyFieldName, StringComparer.OrdinalIgnoreCase))
|
|
|
|
|
|
values[keyFieldName] = GuidGenerator.Create();
|
|
|
|
|
|
|
2026-10-07 14:48:16 +00:00
|
|
|
|
added.Add((values.GetValueOrDefault(keyFieldName) ?? string.Empty, values));
|
2026-09-25 07:52:06 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-07 14:48:16 +00:00
|
|
|
|
// Satir klasorunde her satir anahtariyla adlanan dosyadadir; anahtarsiz satir yazilamaz.
|
2026-10-10 22:03:36 +00:00
|
|
|
|
return added.Count > 0 && (await target.UpsertRowsAsync(listForm.ListFormCode, keyFieldName, added)).Count > 0
|
|
|
|
|
|
? Problem(McpErrorCodes.SeedRowsInvalid)
|
|
|
|
|
|
: null;
|
2026-09-25 07:52:06 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>Satirin anahtar disindaki icerigi; tekrar deploy'da ayni satiri tanimak icin.</summary>
|
|
|
|
|
|
private static string RowContent(Dictionary<string, object> row, string keyFieldName) =>
|
|
|
|
|
|
string.Join('\u001f', row
|
|
|
|
|
|
.Where(item => !string.Equals(item.Key, keyFieldName, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
|
.OrderBy(item => item.Key, StringComparer.OrdinalIgnoreCase)
|
|
|
|
|
|
.Select(item => $"{item.Key.ToLowerInvariant()}={JsonSerializer.Serialize(item.Value)}"));
|
|
|
|
|
|
|
2026-09-24 13:29:53 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Ekranin tablosu ancak <paramref name="dropTables"/> acikken ve Wizard Manager'daki onayla ayni
|
|
|
|
|
|
/// kosullarda (secilebilir aday) dusurulur; onayi kullanicidan almak istemcinin isidir.
|
|
|
|
|
|
/// </summary>
|
2026-10-10 22:03:36 +00:00
|
|
|
|
public async Task<McpOutcome<WizardDeleteResultDto>> DeleteWizardAsync(string fileName, bool dropTables)
|
2026-09-18 13:47:23 +00:00
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (await EnsureToolAllowedAsync(McpToolNames.DeleteWizard) is { Problem: { } problem })
|
|
|
|
|
|
{
|
|
|
|
|
|
return problem;
|
|
|
|
|
|
}
|
2026-09-18 13:47:23 +00:00
|
|
|
|
|
2026-09-24 13:29:53 +00:00
|
|
|
|
var input = new WizardDeleteInput { FileName = fileName };
|
|
|
|
|
|
if (dropTables)
|
|
|
|
|
|
{
|
|
|
|
|
|
var analysis = await wizardAppService.DeleteAnalysis(fileName);
|
|
|
|
|
|
input.DropTables = [.. analysis.Tables.Where(t => t.CanDrop).Select(t => t.TableName)];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-28 12:50:38 +00:00
|
|
|
|
var result = await wizardAppService.DeleteFile(input);
|
|
|
|
|
|
await timesheetRecorder.RecordAsync(McpTimesheetTexts.DeleteWizard, fileName);
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
2026-09-18 13:47:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-24 11:58:23 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Ekrani kullanicinin gorecegi yoldan dener: menu kaydi, menunun istedigi yetki, ekranin
|
|
|
|
|
|
/// sorgusu (cagiranin yetkisi ve kiraci filtresiyle, tek satir) ve Custom yolunda bilesen.
|
|
|
|
|
|
/// Hicbir kontrol veri yazmaz; test satiri eklemek SeedSync acik ekranlarda seed dosyasina
|
|
|
|
|
|
/// iz birakirdi. Seed kapanisi engellemez: ekran bu ortamda calisir, eksik dosya yalnizca
|
|
|
|
|
|
/// migrate ya da baska bir ortamda ortaya cikar.
|
|
|
|
|
|
/// </summary>
|
2026-10-10 22:03:36 +00:00
|
|
|
|
public async Task<McpOutcome<McpVerifyResultDto>> VerifyWizardAsync(string fileName)
|
2026-09-24 11:58:23 +00:00
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (await EnsureToolAllowedAsync(McpToolNames.VerifyWizard) is { Problem: { } problem })
|
|
|
|
|
|
{
|
|
|
|
|
|
return problem;
|
|
|
|
|
|
}
|
2026-09-24 11:58:23 +00:00
|
|
|
|
|
|
|
|
|
|
var result = new McpVerifyResultDto { FileName = fileName };
|
|
|
|
|
|
|
|
|
|
|
|
ListFormWizardDto? wizard;
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
wizard = (await wizardAppService.GetFile(fileName)).Wizard;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
result.Checks.Add(Failed(VerifyChecks.WizardFile, L[McpErrorCodes.VerifyWizardFileInvalid, fileName, ex.Message]));
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (wizard is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
result.Checks.Add(Failed(VerifyChecks.WizardFile, L[McpErrorCodes.InvalidWizard]));
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var listFormCode = wizard.ComponentKind == WizardComponentKindEnum.Custom ? null : wizard.ResolveListFormCode();
|
|
|
|
|
|
result.ListFormCode = listFormCode;
|
|
|
|
|
|
result.MenuUrl = wizard.MenuUrl;
|
|
|
|
|
|
result.Checks.Add(new McpVerifyCheckDto { Name = VerifyChecks.WizardFile, Passed = true });
|
|
|
|
|
|
|
2026-09-25 06:39:34 +00:00
|
|
|
|
// Deploy yalnizca seed dosyalarini yazar; menu, yetki ve ekran DB Migrate'ten sonra vardir.
|
|
|
|
|
|
// Kurulmamis bir wizard'in geri kalan kontrolleri anlamsiz hatalar uretirdi.
|
|
|
|
|
|
var file = (await wizardAppService.GetFiles())
|
|
|
|
|
|
.FirstOrDefault(f => string.Equals(f.FileName, fileName, StringComparison.OrdinalIgnoreCase));
|
|
|
|
|
|
if (file?.IsPending == true)
|
|
|
|
|
|
{
|
|
|
|
|
|
result.Checks.Add(Failed(VerifyChecks.Install, L[McpErrorCodes.VerifyWizardPending, fileName]));
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-24 11:58:23 +00:00
|
|
|
|
var permissionName = listFormCode;
|
|
|
|
|
|
if (wizard.CreateMenu)
|
|
|
|
|
|
{
|
|
|
|
|
|
var menu = await VerifyMenuAsync(wizard, result.Checks);
|
|
|
|
|
|
permissionName = string.IsNullOrWhiteSpace(menu?.RequiredPermissionName) ? permissionName : menu.RequiredPermissionName;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(permissionName))
|
|
|
|
|
|
{
|
|
|
|
|
|
var permission = await permissionRepository.FirstOrDefaultAsync(p => p.Name == permissionName);
|
|
|
|
|
|
result.Checks.Add(permission is { IsEnabled: true }
|
|
|
|
|
|
? new McpVerifyCheckDto { Name = VerifyChecks.Permission, Passed = true, Message = permissionName }
|
|
|
|
|
|
: Failed(VerifyChecks.Permission, L[McpErrorCodes.VerifyPermissionMissing, permissionName]));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result.Checks.Add(listFormCode is null
|
|
|
|
|
|
? await VerifyComponentAsync(wizard)
|
|
|
|
|
|
: await VerifySelectAsync(listFormCode));
|
|
|
|
|
|
|
|
|
|
|
|
result.Checks.Add(await VerifySeedClosureAsync(fileName));
|
|
|
|
|
|
|
|
|
|
|
|
result.Passed = result.Checks.TrueForAll(check => check.Passed || !check.Blocking);
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Durum kurali modelin dikkatine birakilmaz: talep Ready to Start degilse reddedilir, uygunsa
|
|
|
|
|
|
/// ayni unit of work'te In Progress'e alinir. MCP yetki vermez; ekranin guncelleme yetkisi
|
|
|
|
|
|
/// ayrica istenir.
|
|
|
|
|
|
/// </summary>
|
2026-10-10 22:03:36 +00:00
|
|
|
|
public async Task<McpOutcome<McpDevRequestDto>> StartDevRequestAsync(string request)
|
2026-09-24 11:58:23 +00:00
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (await EnsureToolAllowedAsync(McpToolNames.StartDevRequest) is { Problem: { } problem })
|
|
|
|
|
|
{
|
|
|
|
|
|
return problem;
|
|
|
|
|
|
}
|
2026-09-24 11:58:23 +00:00
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (await CheckPermissionAsync(McpToolNames.StartDevRequest, DevRequestUpdatePermission) is { } denied)
|
|
|
|
|
|
{
|
|
|
|
|
|
return denied;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!(await FindDevRequestAsync(request)).TryGetValue(out var devRequest, out var notFound))
|
|
|
|
|
|
{
|
|
|
|
|
|
return notFound;
|
|
|
|
|
|
}
|
2026-09-24 11:58:23 +00:00
|
|
|
|
|
|
|
|
|
|
if (!string.Equals(devRequest.Status, McpConsts.DevRequestStatuses.ReadyToStart, StringComparison.Ordinal))
|
|
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
return Problem(McpErrorCodes.DevRequestNotReady, devRequest.Title ?? devRequest.Id.ToString(), devRequest.Status ?? string.Empty);
|
2026-09-24 11:58:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
devRequest.Status = McpConsts.DevRequestStatuses.InProgress;
|
|
|
|
|
|
await devRequestRepository.UpdateAsync(devRequest, autoSave: true);
|
2026-09-28 12:50:38 +00:00
|
|
|
|
await timesheetRecorder.BeginAsync(devRequest.Id);
|
2026-09-24 11:58:23 +00:00
|
|
|
|
|
|
|
|
|
|
return ToDto(devRequest);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
public async Task<McpOutcome<McpDevRequestDto>> GetDevRequestAsync(string request)
|
2026-09-24 11:58:23 +00:00
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (await EnsureToolAllowedAsync(McpToolNames.GetDevRequest) is { Problem: { } problem })
|
|
|
|
|
|
{
|
|
|
|
|
|
return problem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (await CheckPermissionAsync(McpToolNames.GetDevRequest, PlatformConsts.AppCodes.DevRequests) is { } denied)
|
|
|
|
|
|
{
|
|
|
|
|
|
return denied;
|
|
|
|
|
|
}
|
2026-09-24 11:58:23 +00:00
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (!(await FindDevRequestAsync(request)).TryGetValue(out var devRequest, out var notFound))
|
|
|
|
|
|
{
|
|
|
|
|
|
return notFound;
|
|
|
|
|
|
}
|
2026-09-28 12:50:38 +00:00
|
|
|
|
await timesheetRecorder.BeginAsync(devRequest.Id);
|
|
|
|
|
|
|
|
|
|
|
|
return ToDto(devRequest);
|
2026-09-24 11:58:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-10-09 17:02:27 +00:00
|
|
|
|
/// Durum kurali modelin dikkatine birakilmaz: uretimi baslamamis (Backlog, Analysis, Ready to Start)
|
|
|
|
|
|
/// talep analiz edilir; Ready to Start talebin analizi uretim oncesi yeniden yazilabilir. "Uretime hazir: Evet" analiz uretimin karar vermeden uygulayabilecegi analizdir,
|
2026-10-09 14:01:18 +00:00
|
|
|
|
/// talep dogrudan Ready to Start'a gecer; sartli ya da Hayir olan analiz kullanicinin kararini
|
|
|
|
|
|
/// bekledigi icin Analysis'te kalir.
|
2026-09-28 09:28:56 +00:00
|
|
|
|
/// </summary>
|
2026-10-10 22:03:36 +00:00
|
|
|
|
public async Task<McpOutcome<McpDevRequestDto>> SaveDevRequestAnalysisAsync(string request, string html, bool productionReady)
|
2026-09-28 09:28:56 +00:00
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (await EnsureToolAllowedAsync(McpToolNames.SaveDevRequestAnalysis) is { Problem: { } problem })
|
|
|
|
|
|
{
|
|
|
|
|
|
return problem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (await CheckPermissionAsync(McpToolNames.SaveDevRequestAnalysis, DevRequestUpdatePermission) is { } denied)
|
|
|
|
|
|
{
|
|
|
|
|
|
return denied;
|
|
|
|
|
|
}
|
2026-09-28 09:28:56 +00:00
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(html))
|
|
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
return Problem(McpErrorCodes.DevRequestAnalysisRequired);
|
2026-09-28 09:28:56 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (!(await FindDevRequestAsync(request)).TryGetValue(out var devRequest, out var notFound))
|
|
|
|
|
|
{
|
|
|
|
|
|
return notFound;
|
|
|
|
|
|
}
|
2026-09-28 09:28:56 +00:00
|
|
|
|
|
2026-10-09 17:02:27 +00:00
|
|
|
|
if (devRequest.Status is not (McpConsts.DevRequestStatuses.Backlog or McpConsts.DevRequestStatuses.Analysis or McpConsts.DevRequestStatuses.ReadyToStart))
|
2026-09-28 09:28:56 +00:00
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
return Problem(McpErrorCodes.DevRequestNotAnalyzable, devRequest.Title ?? devRequest.Id.ToString(), devRequest.Status ?? string.Empty);
|
2026-09-28 09:28:56 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
devRequest.Analysis = html.Trim();
|
2026-10-09 14:01:18 +00:00
|
|
|
|
devRequest.Status = productionReady
|
|
|
|
|
|
? McpConsts.DevRequestStatuses.ReadyToStart
|
|
|
|
|
|
: McpConsts.DevRequestStatuses.Analysis;
|
2026-09-28 09:28:56 +00:00
|
|
|
|
await devRequestRepository.UpdateAsync(devRequest, autoSave: true);
|
2026-09-28 12:50:38 +00:00
|
|
|
|
await timesheetRecorder.RecordAsync(McpTimesheetTexts.Analysis);
|
2026-09-28 09:28:56 +00:00
|
|
|
|
|
|
|
|
|
|
return ToDto(devRequest);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Aciklama talebin yazildigi haliyle kalir ve base64 gorsel tasir; uretim notlari analize
|
|
|
|
|
|
/// eklenir. Ekleme sunucuda, var olan metne dokunmadan yapilir.
|
2026-09-24 11:58:23 +00:00
|
|
|
|
/// </summary>
|
2026-10-10 22:03:36 +00:00
|
|
|
|
public async Task<McpProblem?> AppendDevRequestNoteAsync(string request, string html)
|
2026-09-24 11:58:23 +00:00
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (await EnsureToolAllowedAsync(McpToolNames.AppendDevRequestNote) is { Problem: { } problem })
|
|
|
|
|
|
{
|
|
|
|
|
|
return problem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (await CheckPermissionAsync(McpToolNames.AppendDevRequestNote, DevRequestUpdatePermission) is { } denied)
|
|
|
|
|
|
{
|
|
|
|
|
|
return denied;
|
|
|
|
|
|
}
|
2026-09-24 11:58:23 +00:00
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(html))
|
|
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
return Problem(McpErrorCodes.DevRequestNoteRequired);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!(await FindDevRequestAsync(request)).TryGetValue(out var devRequest, out var notFound))
|
|
|
|
|
|
{
|
|
|
|
|
|
return notFound;
|
2026-09-24 11:58:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-28 09:28:56 +00:00
|
|
|
|
devRequest.Analysis = string.Concat(devRequest.Analysis, html.Trim());
|
2026-09-24 11:58:23 +00:00
|
|
|
|
await devRequestRepository.UpdateAsync(devRequest, autoSave: true);
|
2026-09-29 06:14:07 +00:00
|
|
|
|
await timesheetRecorder.RecordAsync(McpTimesheetTexts.AppendNote);
|
2026-10-10 22:03:36 +00:00
|
|
|
|
|
|
|
|
|
|
return null;
|
2026-09-24 11:58:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-24 13:29:53 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Atanan kisi ve tarih modelin dikkatine birakilmaz: satiri yazan cagiran kullanicidir
|
|
|
|
|
|
/// (ekranin atanan listesi kullanici adiyla calisir), bitis tarihi yazildigi gundur.
|
|
|
|
|
|
/// Var olan satirlara dokunulmaz.
|
|
|
|
|
|
/// </summary>
|
2026-10-10 22:03:36 +00:00
|
|
|
|
public async Task<McpProblem?> AddDevRequestSubTasksAsync(string request, List<string> titles)
|
2026-09-24 13:29:53 +00:00
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (await EnsureToolAllowedAsync(McpToolNames.AddDevRequestSubTasks) is { Problem: { } problem })
|
|
|
|
|
|
{
|
|
|
|
|
|
return problem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (await CheckPermissionAsync(McpToolNames.AddDevRequestSubTasks, DevRequestUpdatePermission) is { } denied)
|
|
|
|
|
|
{
|
|
|
|
|
|
return denied;
|
|
|
|
|
|
}
|
2026-09-24 13:29:53 +00:00
|
|
|
|
|
|
|
|
|
|
var items = (titles ?? []).Select(title => title?.Trim()).Where(title => !string.IsNullOrEmpty(title)).ToList();
|
|
|
|
|
|
if (items.Count == 0)
|
|
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
return Problem(McpErrorCodes.DevRequestSubTasksRequired);
|
2026-09-24 13:29:53 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (!(await FindDevRequestAsync(request)).TryGetValue(out var devRequest, out var notFound))
|
|
|
|
|
|
{
|
|
|
|
|
|
return notFound;
|
|
|
|
|
|
}
|
2026-09-24 13:29:53 +00:00
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
JsonArray? subTasks;
|
2026-09-24 13:29:53 +00:00
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
subTasks = string.IsNullOrWhiteSpace(devRequest.SubTasks)
|
2026-10-10 22:03:36 +00:00
|
|
|
|
? new JsonArray()
|
|
|
|
|
|
: JsonNode.Parse(devRequest.SubTasks) as JsonArray;
|
2026-09-24 13:29:53 +00:00
|
|
|
|
}
|
|
|
|
|
|
catch (JsonException)
|
|
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
subTasks = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (subTasks is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return Problem(McpErrorCodes.DevRequestSubTasksInvalid, devRequest.Title ?? devRequest.Id.ToString());
|
2026-09-24 13:29:53 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var assignee = CurrentUser.UserName ?? string.Empty;
|
|
|
|
|
|
var dueDate = Clock.Now.ToString(SubTaskDateFormat, CultureInfo.InvariantCulture);
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var title in items)
|
|
|
|
|
|
{
|
|
|
|
|
|
subTasks.Add(new JsonObject
|
|
|
|
|
|
{
|
|
|
|
|
|
["id"] = GuidGenerator.Create().ToString(),
|
|
|
|
|
|
["title"] = title,
|
|
|
|
|
|
["assignee"] = assignee,
|
|
|
|
|
|
["dueDate"] = dueDate,
|
|
|
|
|
|
["completed"] = true
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
devRequest.SubTasks = subTasks.ToJsonString();
|
|
|
|
|
|
await devRequestRepository.UpdateAsync(devRequest, autoSave: true);
|
2026-09-29 06:14:07 +00:00
|
|
|
|
await timesheetRecorder.RecordAsync(McpTimesheetTexts.AddSubTasks, items.Count);
|
2026-10-10 22:03:36 +00:00
|
|
|
|
|
|
|
|
|
|
return null;
|
2026-09-24 13:29:53 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
public async Task<McpOutcome<ModuleDocDto>> GetModuleDocAsync(string name)
|
2026-10-04 15:04:16 +00:00
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (await EnsureToolAllowedAsync(McpToolNames.GetModuleDoc) is { Problem: { } problem })
|
|
|
|
|
|
{
|
|
|
|
|
|
return problem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-05 19:17:29 +00:00
|
|
|
|
return await moduleDocAppService.GetByNameAsync(name);
|
2026-10-04 15:04:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
public async Task<McpOutcome<ModuleDocDto>> SaveModuleDocAsync(string name, string content)
|
2026-10-04 15:04:16 +00:00
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (await EnsureToolAllowedAsync(McpToolNames.SaveModuleDoc) is { Problem: { } problem })
|
|
|
|
|
|
{
|
|
|
|
|
|
return problem;
|
|
|
|
|
|
}
|
2026-10-04 15:04:16 +00:00
|
|
|
|
|
|
|
|
|
|
var doc = await moduleDocAppService.SaveAsync(new ModuleDocSaveInput { Name = name, Content = content });
|
|
|
|
|
|
await timesheetRecorder.RecordAsync(McpTimesheetTexts.SaveModuleDoc, doc.Name);
|
|
|
|
|
|
|
|
|
|
|
|
return doc;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-24 11:58:23 +00:00
|
|
|
|
private static McpDevRequestDto ToDto(DevRequest devRequest)
|
|
|
|
|
|
{
|
|
|
|
|
|
var (description, images) = McpDevRequestImages.Extract(devRequest.Description);
|
|
|
|
|
|
|
|
|
|
|
|
return new McpDevRequestDto
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = devRequest.Id,
|
|
|
|
|
|
Title = devRequest.Title,
|
2026-10-02 13:58:22 +00:00
|
|
|
|
Status = devRequest.Status ?? string.Empty,
|
2026-09-24 11:58:23 +00:00
|
|
|
|
Description = description,
|
|
|
|
|
|
Images = images,
|
2026-09-28 09:28:56 +00:00
|
|
|
|
Analysis = devRequest.Analysis,
|
2026-09-24 11:58:23 +00:00
|
|
|
|
Priority = devRequest.Priority,
|
|
|
|
|
|
Tags = devRequest.Tags,
|
|
|
|
|
|
DueDate = devRequest.DueDate,
|
|
|
|
|
|
UserName = devRequest.UserName,
|
|
|
|
|
|
TenantId = devRequest.TenantId,
|
|
|
|
|
|
SubTasks = devRequest.SubTasks
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>Id ile ya da tam baslikla bulur; ayni basligi tasiyan birden fazla talep varsa Id istenir.</summary>
|
2026-10-10 22:03:36 +00:00
|
|
|
|
private async Task<McpOutcome<DevRequest>> FindDevRequestAsync(string? request)
|
2026-09-24 11:58:23 +00:00
|
|
|
|
{
|
|
|
|
|
|
var value = request?.Trim() ?? string.Empty;
|
|
|
|
|
|
|
|
|
|
|
|
if (Guid.TryParse(value, out var id))
|
|
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
var byId = await devRequestRepository.FindAsync(id);
|
|
|
|
|
|
return byId is null ? Problem(McpErrorCodes.DevRequestNotFound, value) : byId;
|
2026-09-24 11:58:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var matches = value.Length == 0 ? [] : await devRequestRepository.GetListAsync(r => r.Title == value);
|
|
|
|
|
|
|
|
|
|
|
|
return matches.Count switch
|
|
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
0 => Problem(McpErrorCodes.DevRequestNotFound, value),
|
2026-09-24 11:58:23 +00:00
|
|
|
|
1 => matches[0],
|
2026-10-10 22:03:36 +00:00
|
|
|
|
_ => Problem(McpErrorCodes.DevRequestAmbiguous, value, string.Join(", ", matches.Select(r => r.Id)))
|
2026-09-24 11:58:23 +00:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Kapanis analizi wizard export'unun yetkisini ister; o yetki dogrulamanin kendisini
|
|
|
|
|
|
/// dusurmesin diye yoksa kontrol atlanir ve nedeni raporlanir.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async Task<McpVerifyCheckDto> VerifySeedClosureAsync(string fileName)
|
|
|
|
|
|
{
|
|
|
|
|
|
var check = new McpVerifyCheckDto { Name = VerifyChecks.SeedClosure, Blocking = false };
|
|
|
|
|
|
|
|
|
|
|
|
if (!await AuthorizationService.IsGrantedAsync(PlatformConsts.AppCodes.Listforms.Wizards.Export))
|
|
|
|
|
|
{
|
|
|
|
|
|
check.Message = L[McpErrorCodes.VerifySeedClosureSkipped, PlatformConsts.AppCodes.Listforms.Wizards.Export];
|
|
|
|
|
|
return check;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var warnings = (await wizardAppService.ExportAnalysis(fileName)).Warnings;
|
|
|
|
|
|
if (warnings.Count == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
check.Passed = true;
|
|
|
|
|
|
return check;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
check.Message = L[McpErrorCodes.VerifySeedClosureIncomplete, string.Join(", ", warnings)];
|
|
|
|
|
|
return check;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task<Menu?> VerifyMenuAsync(ListFormWizardDto wizard, List<McpVerifyCheckDto> checks)
|
|
|
|
|
|
{
|
|
|
|
|
|
var menuCode = wizard.ResolveMenuCode();
|
|
|
|
|
|
var menu = await menuRepository.FirstOrDefaultAsync(m => m.Code == menuCode);
|
|
|
|
|
|
|
|
|
|
|
|
if (menu is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
checks.Add(Failed(VerifyChecks.Menu, L[McpErrorCodes.VerifyMenuMissing, menuCode]));
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (menu.IsDisabled)
|
|
|
|
|
|
{
|
|
|
|
|
|
checks.Add(Failed(VerifyChecks.Menu, L[McpErrorCodes.VerifyMenuDisabled, menuCode]));
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (!string.Equals(menu.Url, wizard.MenuUrl, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
|
{
|
|
|
|
|
|
checks.Add(Failed(VerifyChecks.Menu, L[McpErrorCodes.VerifyMenuUrlMismatch, menuCode, menu.Url, wizard.MenuUrl]));
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
checks.Add(new McpVerifyCheckDto { Name = VerifyChecks.Menu, Passed = true, Message = menu.Url });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return menu;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Ekranin sorgusunu query_list ile ayni servisten calistirir; kolon, veri kaynagi ve yetki
|
|
|
|
|
|
/// hatalari burada ortaya cikar.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async Task<McpVerifyCheckDto> VerifySelectAsync(string listFormCode)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var select = await listFormSelectAppService.GetSelectAsync(new SelectRequestDto
|
|
|
|
|
|
{
|
|
|
|
|
|
ListFormCode = listFormCode,
|
|
|
|
|
|
Take = 1,
|
|
|
|
|
|
RequireTotalCount = true
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return new McpVerifyCheckDto
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = VerifyChecks.Select,
|
|
|
|
|
|
Passed = true,
|
|
|
|
|
|
Message = L[McpErrorCodes.VerifySelectPassed, select.TotalCount]
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
return Failed(VerifyChecks.Select, L[McpErrorCodes.VerifySelectFailed, ex.Message]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task<McpVerifyCheckDto> VerifyComponentAsync(ListFormWizardDto wizard)
|
|
|
|
|
|
{
|
|
|
|
|
|
var component = await FindComponentAsync(wizard.CustomComponentName);
|
|
|
|
|
|
|
|
|
|
|
|
if (component is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return Failed(VerifyChecks.Component, L[McpErrorCodes.ComponentNotFound, wizard.CustomComponentName]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!component.IsActive)
|
|
|
|
|
|
{
|
|
|
|
|
|
return Failed(VerifyChecks.Component, L[McpErrorCodes.VerifyComponentInactive, component.Name]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (wizard.CreateMenu && !string.Equals(component.RoutePath, wizard.MenuUrl, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
|
{
|
|
|
|
|
|
return Failed(VerifyChecks.Component, L[McpErrorCodes.VerifyComponentRouteMismatch, component.Name, component.RoutePath ?? string.Empty, wizard.MenuUrl]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return new McpVerifyCheckDto { Name = VerifyChecks.Component, Passed = true, Message = component.RoutePath };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static McpVerifyCheckDto Failed(string name, string message)
|
|
|
|
|
|
=> new() { Name = name, Passed = false, Message = message };
|
|
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
public async Task<McpOutcome<List<McpComponentSummaryDto>>> GetComponentsAsync(string? search)
|
2026-09-18 13:47:23 +00:00
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (await EnsureToolAllowedAsync(McpToolNames.ListComponents) is { Problem: { } problem })
|
|
|
|
|
|
{
|
|
|
|
|
|
return problem;
|
|
|
|
|
|
}
|
2026-09-18 13:47:23 +00:00
|
|
|
|
|
|
|
|
|
|
var components = await GetAllComponentsAsync();
|
|
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
return components
|
2026-09-18 13:47:23 +00:00
|
|
|
|
.Where(component => string.IsNullOrWhiteSpace(search)
|
|
|
|
|
|
|| Contains(component.Name, search)
|
|
|
|
|
|
|| Contains(component.RoutePath, search)
|
|
|
|
|
|
|| Contains(component.Description, search))
|
|
|
|
|
|
.Select(component => new McpComponentSummaryDto
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = component.Id,
|
|
|
|
|
|
Name = component.Name,
|
|
|
|
|
|
RoutePath = component.RoutePath,
|
|
|
|
|
|
Description = component.Description,
|
|
|
|
|
|
IsActive = component.IsActive,
|
|
|
|
|
|
IsVisualDesigner = IsVisualDesignerDocument(component.Props)
|
2026-10-10 22:03:36 +00:00
|
|
|
|
})
|
|
|
|
|
|
.ToList();
|
2026-09-18 13:47:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
public async Task<McpOutcome<CustomComponentDto?>> GetComponentAsync(string name)
|
2026-09-18 13:47:23 +00:00
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (await EnsureToolAllowedAsync(McpToolNames.GetComponent) is { Problem: { } problem })
|
|
|
|
|
|
{
|
|
|
|
|
|
return problem;
|
|
|
|
|
|
}
|
2026-09-18 13:47:23 +00:00
|
|
|
|
|
|
|
|
|
|
return await FindComponentAsync(name);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
public async Task<McpOutcome<CustomComponentDto>> SaveComponentAsync(McpComponentSaveRequestDto input)
|
2026-09-18 13:47:23 +00:00
|
|
|
|
{
|
|
|
|
|
|
Check.NotNull(input, nameof(input));
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (await EnsureToolAllowedAsync(McpToolNames.SaveComponent) is { Problem: { } problem })
|
|
|
|
|
|
{
|
|
|
|
|
|
return problem;
|
|
|
|
|
|
}
|
2026-09-18 13:47:23 +00:00
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(input.Name))
|
|
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
return Problem(McpErrorCodes.ComponentNameRequired);
|
2026-09-18 13:47:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var existing = await FindComponentAsync(input.Name);
|
|
|
|
|
|
var props = input.Props ?? existing?.Props;
|
|
|
|
|
|
|
|
|
|
|
|
// Tasarimci belgesinde kod belgeden uretilir; buraya yazilan kod tasarimcidaki ilk
|
|
|
|
|
|
// kayitta sessizce ezilirdi. Belge MCP'de uretilemedigi icin bu bilesenler tasarimcida kalir.
|
|
|
|
|
|
if (IsVisualDesignerDocument(props))
|
|
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
return Problem(McpErrorCodes.VisualDesignerComponent, input.Name);
|
2026-09-18 13:47:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (existing is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(input.Code))
|
|
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
return Problem(McpErrorCodes.ComponentCodeRequired);
|
2026-09-18 13:47:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-28 12:50:38 +00:00
|
|
|
|
var created = await componentAppService.CreateAsync(new CreateUpdateCustomComponentDto
|
2026-09-18 13:47:23 +00:00
|
|
|
|
{
|
|
|
|
|
|
Name = input.Name.Trim(),
|
|
|
|
|
|
Code = input.Code,
|
|
|
|
|
|
RoutePath = input.RoutePath,
|
|
|
|
|
|
Description = input.Description,
|
|
|
|
|
|
Props = props,
|
|
|
|
|
|
Dependencies = input.Dependencies,
|
|
|
|
|
|
IsActive = input.IsActive ?? true
|
|
|
|
|
|
});
|
2026-09-28 12:50:38 +00:00
|
|
|
|
await timesheetRecorder.RecordAsync(McpTimesheetTexts.SaveComponent, created.Name);
|
|
|
|
|
|
|
|
|
|
|
|
return created;
|
2026-09-18 13:47:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-28 12:50:38 +00:00
|
|
|
|
var updated = await componentAppService.UpdateAsync(existing.Id, new CreateUpdateCustomComponentDto
|
2026-09-18 13:47:23 +00:00
|
|
|
|
{
|
|
|
|
|
|
Name = existing.Name,
|
|
|
|
|
|
Code = string.IsNullOrWhiteSpace(input.Code) ? existing.Code : input.Code,
|
|
|
|
|
|
RoutePath = input.RoutePath ?? existing.RoutePath,
|
|
|
|
|
|
Description = input.Description ?? existing.Description,
|
|
|
|
|
|
Props = props,
|
|
|
|
|
|
Dependencies = input.Dependencies ?? existing.Dependencies,
|
|
|
|
|
|
IsActive = input.IsActive ?? existing.IsActive
|
|
|
|
|
|
});
|
2026-09-28 12:50:38 +00:00
|
|
|
|
await timesheetRecorder.RecordAsync(McpTimesheetTexts.SaveComponent, updated.Name);
|
|
|
|
|
|
|
|
|
|
|
|
return updated;
|
2026-09-18 13:47:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
public async Task<McpProblem?> DeleteComponentAsync(string name)
|
2026-09-18 13:47:23 +00:00
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (await EnsureToolAllowedAsync(McpToolNames.DeleteComponent) is { Problem: { } problem })
|
|
|
|
|
|
{
|
|
|
|
|
|
return problem;
|
|
|
|
|
|
}
|
2026-09-18 13:47:23 +00:00
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
var component = await FindComponentAsync(name);
|
|
|
|
|
|
if (component is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return Problem(McpErrorCodes.ComponentNotFound, name);
|
|
|
|
|
|
}
|
2026-09-18 13:47:23 +00:00
|
|
|
|
|
|
|
|
|
|
await componentAppService.DeleteAsync(component.Id);
|
2026-09-28 12:50:38 +00:00
|
|
|
|
await timesheetRecorder.RecordAsync(McpTimesheetTexts.DeleteComponent, component.Name);
|
2026-10-10 22:03:36 +00:00
|
|
|
|
|
|
|
|
|
|
return null;
|
2026-09-18 13:47:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-10-07 18:46:45 +00:00
|
|
|
|
/// Wizard'i seed dosyasi biciminden (<c>{"Wizard": {...}, "ListForm": {...}, "Fields": [...]}</c>)
|
|
|
|
|
|
/// ya da duz cevaplardan okur; Menu/Language bloklari ve WizardCode/ModuleId adlari dosyadakiyle
|
|
|
|
|
|
/// ayni anlama gelir. ListForm/Fields bolumleri ekrana katman olarak uygulanir
|
|
|
|
|
|
/// (<see cref="WizardSeedFileDto.FromDeployJson"/>).
|
2026-09-18 13:47:23 +00:00
|
|
|
|
/// </summary>
|
2026-10-10 22:03:36 +00:00
|
|
|
|
private McpOutcome<ListFormWizardDto> ParseWizard(string? json)
|
2026-09-18 13:47:23 +00:00
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
ListFormWizardDto? wizard;
|
2026-09-18 13:47:23 +00:00
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
wizard = string.IsNullOrWhiteSpace(json) ? null : WizardSeedFileDto.FromDeployJson(json);
|
2026-09-18 13:47:23 +00:00
|
|
|
|
}
|
2026-10-09 11:23:24 +00:00
|
|
|
|
catch (JsonException ex)
|
2026-09-18 13:47:23 +00:00
|
|
|
|
{
|
2026-10-09 11:23:24 +00:00
|
|
|
|
// Genel mesajla model hangi degerin bozuk oldugunu bilemez ve ayni JSON'u tekrar gonderir.
|
2026-10-10 22:03:36 +00:00
|
|
|
|
return Problem(McpErrorCodes.InvalidWizardValue, ex.Path ?? "$", ex.Message);
|
2026-09-18 13:47:23 +00:00
|
|
|
|
}
|
2026-10-10 22:03:36 +00:00
|
|
|
|
|
|
|
|
|
|
return wizard is null ? Problem(McpErrorCodes.InvalidWizard) : wizard;
|
2026-09-18 13:47:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task<IReadOnlyList<CustomComponentDto>> GetAllComponentsAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
var page = await componentAppService.GetListAsync(new PagedAndSortedResultRequestDto
|
|
|
|
|
|
{
|
|
|
|
|
|
MaxResultCount = ComponentPageSize,
|
|
|
|
|
|
Sorting = nameof(CustomComponentDto.Name)
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return page.Items;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task<CustomComponentDto?> FindComponentAsync(string? name)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
|
|
|
|
{
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var components = await GetAllComponentsAsync();
|
|
|
|
|
|
|
|
|
|
|
|
return components.FirstOrDefault(component => string.Equals(component.Name, name.Trim(), StringComparison.OrdinalIgnoreCase));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Props icinde tasarimci belgesi varsa ve kip acikca <c>code</c> degilse kod belgeden
|
|
|
|
|
|
/// uretilir; tasarimcinin varsayilan kipi <c>visual</c>'dir.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private static bool IsVisualDesignerDocument(string? props)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(props))
|
|
|
|
|
|
{
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
using var document = JsonDocument.Parse(props);
|
|
|
|
|
|
if (document.RootElement.ValueKind != JsonValueKind.Object ||
|
|
|
|
|
|
!document.RootElement.TryGetProperty("visualDesigner", out var designer) ||
|
|
|
|
|
|
designer.ValueKind != JsonValueKind.Object)
|
|
|
|
|
|
{
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return !designer.TryGetProperty("sourceMode", out var mode) ||
|
|
|
|
|
|
mode.ValueKind != JsonValueKind.String ||
|
|
|
|
|
|
!string.Equals(mode.GetString(), "code", StringComparison.Ordinal);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (JsonException)
|
|
|
|
|
|
{
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static bool Contains(string? value, string search)
|
|
|
|
|
|
=> value?.Contains(search.Trim(), StringComparison.OrdinalIgnoreCase) == true;
|
|
|
|
|
|
|
|
|
|
|
|
private static McpDevGuideHeadingDto ToDto(McpDevGuideHeading heading) => new()
|
|
|
|
|
|
{
|
|
|
|
|
|
Document = heading.Document,
|
|
|
|
|
|
Id = heading.Id,
|
|
|
|
|
|
Title = heading.Title,
|
|
|
|
|
|
Level = heading.Level
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|