Expand settings UI to manage systems, containers, and certificate paths.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -595,4 +595,730 @@ public sealed class SonicSetupRepository
|
||||
reader.GetOrdinal("IsActive"))
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<CompanyOption>> GetCompaniesAsync(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT
|
||||
[CompanyId],
|
||||
[CompanyCode],
|
||||
[CompanyName]
|
||||
FROM [dbo].[Company]
|
||||
WHERE [IsActive] = 1
|
||||
ORDER BY [CompanyCode];
|
||||
""";
|
||||
|
||||
List<CompanyOption> companies = [];
|
||||
|
||||
await using SqlConnection connection =
|
||||
new(_connectionString);
|
||||
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
await using SqlCommand command =
|
||||
new(sql, connection);
|
||||
|
||||
await using SqlDataReader reader =
|
||||
await command.ExecuteReaderAsync(cancellationToken);
|
||||
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
companies.Add(
|
||||
new CompanyOption
|
||||
{
|
||||
CompanyId =
|
||||
reader.GetInt32(
|
||||
reader.GetOrdinal("CompanyId")),
|
||||
|
||||
CompanyCode =
|
||||
reader.GetString(
|
||||
reader.GetOrdinal("CompanyCode")),
|
||||
|
||||
CompanyName =
|
||||
reader.GetString(
|
||||
reader.GetOrdinal("CompanyName"))
|
||||
});
|
||||
}
|
||||
|
||||
return companies;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<ContainerOption>> GetContainersAsync(
|
||||
string environmentCode,
|
||||
int? sonicConnectionId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(environmentCode))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Der EnvironmentCode fehlt.",
|
||||
nameof(environmentCode));
|
||||
}
|
||||
|
||||
const string sql = """
|
||||
SELECT
|
||||
container.[SonicContainerId],
|
||||
container.[SonicConnectionId],
|
||||
container.[CompanyId],
|
||||
container.[ContainerName],
|
||||
container.[ContainerDisplayName],
|
||||
container.[RestartTimeoutSeconds],
|
||||
company.[CompanyCode],
|
||||
connection.[ConnectionName]
|
||||
FROM [dbo].[SonicContainer] AS container
|
||||
INNER JOIN [dbo].[Company] AS company
|
||||
ON company.[CompanyId] = container.[CompanyId]
|
||||
INNER JOIN [dbo].[SonicConnection] AS connection
|
||||
ON connection.[SonicConnectionId] =
|
||||
container.[SonicConnectionId]
|
||||
INNER JOIN [dbo].[Environment] AS environment
|
||||
ON environment.[EnvironmentId] =
|
||||
connection.[EnvironmentId]
|
||||
WHERE container.[IsActive] = 1
|
||||
AND company.[IsActive] = 1
|
||||
AND connection.[IsActive] = 1
|
||||
AND environment.[IsActive] = 1
|
||||
AND environment.[EnvironmentCode] = @EnvironmentCode
|
||||
AND
|
||||
(
|
||||
@SonicConnectionId IS NULL
|
||||
OR container.[SonicConnectionId] = @SonicConnectionId
|
||||
)
|
||||
ORDER BY
|
||||
company.[CompanyCode],
|
||||
container.[ContainerName];
|
||||
""";
|
||||
|
||||
List<ContainerOption> containers = [];
|
||||
|
||||
await using SqlConnection connection =
|
||||
new(_connectionString);
|
||||
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
await using SqlCommand command =
|
||||
new(sql, connection);
|
||||
|
||||
command.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@EnvironmentCode",
|
||||
SqlDbType.NVarChar,
|
||||
30)
|
||||
{
|
||||
Value = environmentCode
|
||||
.Trim()
|
||||
.ToUpperInvariant()
|
||||
});
|
||||
|
||||
command.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@SonicConnectionId",
|
||||
SqlDbType.Int)
|
||||
{
|
||||
Value = sonicConnectionId is null or <= 0
|
||||
? DBNull.Value
|
||||
: sonicConnectionId.Value
|
||||
});
|
||||
|
||||
await using SqlDataReader reader =
|
||||
await command.ExecuteReaderAsync(cancellationToken);
|
||||
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
int displayOrdinal =
|
||||
reader.GetOrdinal("ContainerDisplayName");
|
||||
|
||||
containers.Add(
|
||||
new ContainerOption
|
||||
{
|
||||
SonicContainerId =
|
||||
reader.GetInt32(
|
||||
reader.GetOrdinal("SonicContainerId")),
|
||||
|
||||
SonicConnectionId =
|
||||
reader.GetInt32(
|
||||
reader.GetOrdinal("SonicConnectionId")),
|
||||
|
||||
CompanyId =
|
||||
reader.GetInt32(
|
||||
reader.GetOrdinal("CompanyId")),
|
||||
|
||||
ContainerName =
|
||||
reader.GetString(
|
||||
reader.GetOrdinal("ContainerName")),
|
||||
|
||||
ContainerDisplayName =
|
||||
reader.IsDBNull(displayOrdinal)
|
||||
? null
|
||||
: reader.GetString(displayOrdinal),
|
||||
|
||||
RestartTimeoutSeconds =
|
||||
reader.GetInt32(
|
||||
reader.GetOrdinal("RestartTimeoutSeconds")),
|
||||
|
||||
CompanyCode =
|
||||
reader.GetString(
|
||||
reader.GetOrdinal("CompanyCode")),
|
||||
|
||||
ConnectionName =
|
||||
reader.GetString(
|
||||
reader.GetOrdinal("ConnectionName"))
|
||||
});
|
||||
}
|
||||
|
||||
return containers;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<CertificateTargetOption>>
|
||||
GetCertificateTargetsAsync(
|
||||
string environmentCode,
|
||||
int? sonicContainerId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(environmentCode))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Der EnvironmentCode fehlt.",
|
||||
nameof(environmentCode));
|
||||
}
|
||||
|
||||
const string sql = """
|
||||
SELECT
|
||||
target.[CertificateTargetId],
|
||||
target.[SonicContainerId],
|
||||
target.[TargetDirectory],
|
||||
target.[TargetFileName],
|
||||
target.[BackupEnabled],
|
||||
target.[BackupDirectoryName],
|
||||
container.[ContainerName],
|
||||
connection.[ConnectionName],
|
||||
company.[CompanyCode]
|
||||
FROM [dbo].[CertificateTarget] AS target
|
||||
INNER JOIN [dbo].[SonicContainer] AS container
|
||||
ON container.[SonicContainerId] =
|
||||
target.[SonicContainerId]
|
||||
INNER JOIN [dbo].[SonicConnection] AS connection
|
||||
ON connection.[SonicConnectionId] =
|
||||
container.[SonicConnectionId]
|
||||
INNER JOIN [dbo].[Company] AS company
|
||||
ON company.[CompanyId] = container.[CompanyId]
|
||||
INNER JOIN [dbo].[Environment] AS environment
|
||||
ON environment.[EnvironmentId] =
|
||||
connection.[EnvironmentId]
|
||||
WHERE target.[IsActive] = 1
|
||||
AND container.[IsActive] = 1
|
||||
AND connection.[IsActive] = 1
|
||||
AND company.[IsActive] = 1
|
||||
AND environment.[IsActive] = 1
|
||||
AND environment.[EnvironmentCode] = @EnvironmentCode
|
||||
AND
|
||||
(
|
||||
@SonicContainerId IS NULL
|
||||
OR target.[SonicContainerId] = @SonicContainerId
|
||||
)
|
||||
ORDER BY
|
||||
company.[CompanyCode],
|
||||
container.[ContainerName],
|
||||
target.[TargetDirectory],
|
||||
target.[TargetFileName];
|
||||
""";
|
||||
|
||||
List<CertificateTargetOption> targets = [];
|
||||
|
||||
await using SqlConnection connection =
|
||||
new(_connectionString);
|
||||
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
await using SqlCommand command =
|
||||
new(sql, connection);
|
||||
|
||||
command.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@EnvironmentCode",
|
||||
SqlDbType.NVarChar,
|
||||
30)
|
||||
{
|
||||
Value = environmentCode
|
||||
.Trim()
|
||||
.ToUpperInvariant()
|
||||
});
|
||||
|
||||
command.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@SonicContainerId",
|
||||
SqlDbType.Int)
|
||||
{
|
||||
Value = sonicContainerId is null or <= 0
|
||||
? DBNull.Value
|
||||
: sonicContainerId.Value
|
||||
});
|
||||
|
||||
await using SqlDataReader reader =
|
||||
await command.ExecuteReaderAsync(cancellationToken);
|
||||
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
targets.Add(
|
||||
new CertificateTargetOption
|
||||
{
|
||||
CertificateTargetId =
|
||||
reader.GetInt32(
|
||||
reader.GetOrdinal("CertificateTargetId")),
|
||||
|
||||
SonicContainerId =
|
||||
reader.GetInt32(
|
||||
reader.GetOrdinal("SonicContainerId")),
|
||||
|
||||
TargetDirectory =
|
||||
reader.GetString(
|
||||
reader.GetOrdinal("TargetDirectory")),
|
||||
|
||||
TargetFileName =
|
||||
reader.GetString(
|
||||
reader.GetOrdinal("TargetFileName")),
|
||||
|
||||
BackupEnabled =
|
||||
reader.GetBoolean(
|
||||
reader.GetOrdinal("BackupEnabled")),
|
||||
|
||||
BackupDirectoryName =
|
||||
reader.GetString(
|
||||
reader.GetOrdinal("BackupDirectoryName")),
|
||||
|
||||
ContainerName =
|
||||
reader.GetString(
|
||||
reader.GetOrdinal("ContainerName")),
|
||||
|
||||
ConnectionName =
|
||||
reader.GetString(
|
||||
reader.GetOrdinal("ConnectionName")),
|
||||
|
||||
CompanyCode =
|
||||
reader.GetString(
|
||||
reader.GetOrdinal("CompanyCode"))
|
||||
});
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
public async Task<int> AddSonicConnectionAsync(
|
||||
string environmentCode,
|
||||
string connectionName,
|
||||
string managementHost,
|
||||
int managementPort,
|
||||
string domainName,
|
||||
string connectionProtocol,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(environmentCode))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Der EnvironmentCode fehlt.",
|
||||
nameof(environmentCode));
|
||||
}
|
||||
|
||||
string name = RequireTrimmed(
|
||||
connectionName,
|
||||
150,
|
||||
"ConnectionName");
|
||||
|
||||
string host = RequireTrimmed(
|
||||
managementHost,
|
||||
255,
|
||||
"ManagementHost");
|
||||
|
||||
string domain = RequireTrimmed(
|
||||
domainName,
|
||||
150,
|
||||
"DomainName");
|
||||
|
||||
string protocol = string.IsNullOrWhiteSpace(connectionProtocol)
|
||||
? "tcp"
|
||||
: connectionProtocol.Trim().TrimEnd(':', '/');
|
||||
|
||||
if (managementPort is < 1 or > 65535)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(managementPort),
|
||||
"Der ManagementPort muss zwischen 1 und 65535 liegen.");
|
||||
}
|
||||
|
||||
const string sql = """
|
||||
INSERT INTO [dbo].[SonicConnection]
|
||||
(
|
||||
[EnvironmentId],
|
||||
[ConnectionName],
|
||||
[ManagementHost],
|
||||
[ManagementPort],
|
||||
[DomainName],
|
||||
[ConnectionProtocol],
|
||||
[IsActive],
|
||||
[CreationDateTime],
|
||||
[CreatedBy]
|
||||
)
|
||||
OUTPUT INSERTED.[SonicConnectionId]
|
||||
SELECT
|
||||
environment.[EnvironmentId],
|
||||
@ConnectionName,
|
||||
@ManagementHost,
|
||||
@ManagementPort,
|
||||
@DomainName,
|
||||
@ConnectionProtocol,
|
||||
1,
|
||||
SYSUTCDATETIME(),
|
||||
SUSER_SNAME()
|
||||
FROM [dbo].[Environment] AS environment
|
||||
WHERE environment.[EnvironmentCode] = @EnvironmentCode
|
||||
AND environment.[IsActive] = 1;
|
||||
""";
|
||||
|
||||
await using SqlConnection connection =
|
||||
new(_connectionString);
|
||||
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
await using SqlCommand command =
|
||||
new(sql, connection);
|
||||
|
||||
command.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@EnvironmentCode",
|
||||
SqlDbType.NVarChar,
|
||||
30)
|
||||
{
|
||||
Value = environmentCode
|
||||
.Trim()
|
||||
.ToUpperInvariant()
|
||||
});
|
||||
|
||||
command.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@ConnectionName",
|
||||
SqlDbType.NVarChar,
|
||||
150)
|
||||
{
|
||||
Value = name
|
||||
});
|
||||
|
||||
command.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@ManagementHost",
|
||||
SqlDbType.NVarChar,
|
||||
255)
|
||||
{
|
||||
Value = host
|
||||
});
|
||||
|
||||
command.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@ManagementPort",
|
||||
SqlDbType.Int)
|
||||
{
|
||||
Value = managementPort
|
||||
});
|
||||
|
||||
command.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@DomainName",
|
||||
SqlDbType.NVarChar,
|
||||
150)
|
||||
{
|
||||
Value = domain
|
||||
});
|
||||
|
||||
command.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@ConnectionProtocol",
|
||||
SqlDbType.NVarChar,
|
||||
20)
|
||||
{
|
||||
Value = protocol
|
||||
});
|
||||
|
||||
object? result =
|
||||
await command.ExecuteScalarAsync(cancellationToken);
|
||||
|
||||
if (result is null || result is DBNull)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Die Umgebung '{environmentCode}' wurde nicht gefunden " +
|
||||
"oder die Sonic-Verbindung konnte nicht angelegt werden.");
|
||||
}
|
||||
|
||||
return Convert.ToInt32(result);
|
||||
}
|
||||
|
||||
public async Task<int> AddSonicContainerAsync(
|
||||
int companyId,
|
||||
int sonicConnectionId,
|
||||
string containerName,
|
||||
string? containerDisplayName,
|
||||
int restartTimeoutSeconds,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (companyId <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(companyId));
|
||||
}
|
||||
|
||||
if (sonicConnectionId <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(sonicConnectionId));
|
||||
}
|
||||
|
||||
string name = RequireTrimmed(
|
||||
containerName,
|
||||
250,
|
||||
"ContainerName");
|
||||
|
||||
string? display =
|
||||
string.IsNullOrWhiteSpace(containerDisplayName)
|
||||
? null
|
||||
: containerDisplayName.Trim();
|
||||
|
||||
if (display is { Length: > 250 })
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Der Anzeigename darf maximal 250 Zeichen enthalten.",
|
||||
nameof(containerDisplayName));
|
||||
}
|
||||
|
||||
if (restartTimeoutSeconds is < 10 or > 3600)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(restartTimeoutSeconds),
|
||||
"Timeout muss zwischen 10 und 3600 Sekunden liegen.");
|
||||
}
|
||||
|
||||
const string sql = """
|
||||
INSERT INTO [dbo].[SonicContainer]
|
||||
(
|
||||
[CompanyId],
|
||||
[SonicConnectionId],
|
||||
[ContainerName],
|
||||
[ContainerDisplayName],
|
||||
[RestartTimeoutSeconds],
|
||||
[IsActive],
|
||||
[CreationDateTime],
|
||||
[CreatedBy]
|
||||
)
|
||||
OUTPUT INSERTED.[SonicContainerId]
|
||||
VALUES
|
||||
(
|
||||
@CompanyId,
|
||||
@SonicConnectionId,
|
||||
@ContainerName,
|
||||
@ContainerDisplayName,
|
||||
@RestartTimeoutSeconds,
|
||||
1,
|
||||
SYSUTCDATETIME(),
|
||||
SUSER_SNAME()
|
||||
);
|
||||
""";
|
||||
|
||||
await using SqlConnection connection =
|
||||
new(_connectionString);
|
||||
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
await using SqlCommand command =
|
||||
new(sql, connection);
|
||||
|
||||
command.Parameters.Add(
|
||||
new SqlParameter("@CompanyId", SqlDbType.Int)
|
||||
{
|
||||
Value = companyId
|
||||
});
|
||||
|
||||
command.Parameters.Add(
|
||||
new SqlParameter("@SonicConnectionId", SqlDbType.Int)
|
||||
{
|
||||
Value = sonicConnectionId
|
||||
});
|
||||
|
||||
command.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@ContainerName",
|
||||
SqlDbType.NVarChar,
|
||||
250)
|
||||
{
|
||||
Value = name
|
||||
});
|
||||
|
||||
command.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@ContainerDisplayName",
|
||||
SqlDbType.NVarChar,
|
||||
250)
|
||||
{
|
||||
Value = display is null
|
||||
? DBNull.Value
|
||||
: display
|
||||
});
|
||||
|
||||
command.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@RestartTimeoutSeconds",
|
||||
SqlDbType.Int)
|
||||
{
|
||||
Value = restartTimeoutSeconds
|
||||
});
|
||||
|
||||
object? result =
|
||||
await command.ExecuteScalarAsync(cancellationToken);
|
||||
|
||||
if (result is null || result is DBNull)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Der Container konnte nicht angelegt werden.");
|
||||
}
|
||||
|
||||
return Convert.ToInt32(result);
|
||||
}
|
||||
|
||||
public async Task<int> AddCertificateTargetAsync(
|
||||
int sonicContainerId,
|
||||
string targetDirectory,
|
||||
string targetFileName,
|
||||
bool backupEnabled,
|
||||
string backupDirectoryName,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (sonicContainerId <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(sonicContainerId));
|
||||
}
|
||||
|
||||
string directory = RequireTrimmed(
|
||||
targetDirectory,
|
||||
500,
|
||||
"TargetDirectory");
|
||||
|
||||
string fileName = RequireTrimmed(
|
||||
targetFileName,
|
||||
260,
|
||||
"TargetFileName");
|
||||
|
||||
string backupName = string.IsNullOrWhiteSpace(backupDirectoryName)
|
||||
? "Backup"
|
||||
: backupDirectoryName.Trim();
|
||||
|
||||
if (backupName.Length > 100)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Der Backup-Ordnername darf maximal 100 Zeichen enthalten.",
|
||||
nameof(backupDirectoryName));
|
||||
}
|
||||
|
||||
const string sql = """
|
||||
INSERT INTO [dbo].[CertificateTarget]
|
||||
(
|
||||
[SonicContainerId],
|
||||
[TargetDirectory],
|
||||
[TargetFileName],
|
||||
[BackupEnabled],
|
||||
[BackupDirectoryName],
|
||||
[IsActive],
|
||||
[CreationDateTime],
|
||||
[CreatedBy]
|
||||
)
|
||||
OUTPUT INSERTED.[CertificateTargetId]
|
||||
VALUES
|
||||
(
|
||||
@SonicContainerId,
|
||||
@TargetDirectory,
|
||||
@TargetFileName,
|
||||
@BackupEnabled,
|
||||
@BackupDirectoryName,
|
||||
1,
|
||||
SYSUTCDATETIME(),
|
||||
SUSER_SNAME()
|
||||
);
|
||||
""";
|
||||
|
||||
await using SqlConnection connection =
|
||||
new(_connectionString);
|
||||
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
await using SqlCommand command =
|
||||
new(sql, connection);
|
||||
|
||||
command.Parameters.Add(
|
||||
new SqlParameter("@SonicContainerId", SqlDbType.Int)
|
||||
{
|
||||
Value = sonicContainerId
|
||||
});
|
||||
|
||||
command.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@TargetDirectory",
|
||||
SqlDbType.NVarChar,
|
||||
500)
|
||||
{
|
||||
Value = directory
|
||||
});
|
||||
|
||||
command.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@TargetFileName",
|
||||
SqlDbType.NVarChar,
|
||||
260)
|
||||
{
|
||||
Value = fileName
|
||||
});
|
||||
|
||||
command.Parameters.Add(
|
||||
new SqlParameter("@BackupEnabled", SqlDbType.Bit)
|
||||
{
|
||||
Value = backupEnabled
|
||||
});
|
||||
|
||||
command.Parameters.Add(
|
||||
new SqlParameter(
|
||||
"@BackupDirectoryName",
|
||||
SqlDbType.NVarChar,
|
||||
100)
|
||||
{
|
||||
Value = backupName
|
||||
});
|
||||
|
||||
object? result =
|
||||
await command.ExecuteScalarAsync(cancellationToken);
|
||||
|
||||
if (result is null || result is DBNull)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Das Zertifikat-Ziel konnte nicht angelegt werden.");
|
||||
}
|
||||
|
||||
return Convert.ToInt32(result);
|
||||
}
|
||||
|
||||
private static string RequireTrimmed(
|
||||
string? value,
|
||||
int maxLength,
|
||||
string fieldName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"'{fieldName}' fehlt.");
|
||||
}
|
||||
|
||||
string trimmed = value.Trim();
|
||||
|
||||
if (trimmed.Length > maxLength)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"'{fieldName}' darf maximal {maxLength} Zeichen enthalten.");
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user