Improve admin candidate modal UX and add clip menu visibility toggle
- Widen AdminCandidateEditorModal to size lg for better readability - Rename "Clip-Compilation" section to "Clip / Compilation", update copy to reflect single clips too, drop upload hint and Clip-Plattform field, rename label to "Link" - Fix NativeSelect dropdown clipping inside overflow-y-auto modals by teleporting the menu to body with fixed positioning, flip-up logic, and dynamic maxHeight capped to viewport - Add ClipAdminMenuVisible setting (backend domain, contracts, endpoint, migration) with matching frontend types, defaults, form wiring, and toggle in the Clip-Workflow modal — hides the Clips nav item from the admin sidebar when disabled Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
using System.Text.Json;
|
||||
using Backend.Domain;
|
||||
|
||||
namespace Backend.Services;
|
||||
|
||||
public sealed record WorkflowRuleSetting(
|
||||
string Key,
|
||||
string Label,
|
||||
bool Enabled,
|
||||
int Limit,
|
||||
string Mode,
|
||||
string Description);
|
||||
|
||||
public static class WorkflowRuleSettings
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
public const string MaxFinalistsPerCategory = "max_finalists_per_category";
|
||||
public const string MaxCandidateAppearances = "max_candidate_appearances";
|
||||
public const string MaxWinnerPlacements = "max_winner_placements";
|
||||
public const string WinnerRequiresClip = "winner_requires_clip";
|
||||
|
||||
public static WorkflowRuleSetting[] Defaults { get; } =
|
||||
[
|
||||
new(MaxFinalistsPerCategory, "Finale Kandidat:innen pro Kategorie", true, 4, "block", "Begrenzt, wie viele finale Kandidat:innen in einer Kategorie vorbereitet werden."),
|
||||
new(MaxCandidateAppearances, "Kandidaturen pro Person", true, 2, "warn", "Warnt oder blockiert, wenn dieselbe Person in zu vielen Kategorien final auftaucht."),
|
||||
new(MaxWinnerPlacements, "Gewinnerplätze pro Person", true, 1, "block", "Verhindert oder warnt, wenn dieselbe Person mehrfach als Gewinner:in gesetzt wird."),
|
||||
new(WinnerRequiresClip, "Gewinner braucht Clip-Link", true, 1, "block", "Verhindert oder warnt, wenn ein Gewinner ohne gepflegten YouTube-/Twitch-Clip gesetzt wird."),
|
||||
];
|
||||
|
||||
public static WorkflowRuleSetting[] Read(SiteSettings? settings)
|
||||
{
|
||||
var storedRules = Parse(settings?.WorkflowRulesJson);
|
||||
return Defaults
|
||||
.Select(defaultRule =>
|
||||
{
|
||||
var storedRule = storedRules.FirstOrDefault(item => string.Equals(item.Key, defaultRule.Key, StringComparison.OrdinalIgnoreCase));
|
||||
return storedRule is null ? defaultRule : Normalize(storedRule, defaultRule);
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public static string Serialize(IEnumerable<WorkflowRuleSetting> rules) =>
|
||||
JsonSerializer.Serialize(rules.Select(rule => Normalize(rule, Defaults.FirstOrDefault(item => item.Key == rule.Key) ?? rule)), JsonOptions);
|
||||
|
||||
public static WorkflowRuleSetting Find(IEnumerable<WorkflowRuleSetting> rules, string key) =>
|
||||
rules.FirstOrDefault(item => string.Equals(item.Key, key, StringComparison.OrdinalIgnoreCase))
|
||||
?? Defaults.First(item => item.Key == key);
|
||||
|
||||
public static bool ShouldBlock(WorkflowRuleSetting rule) =>
|
||||
rule.Enabled && string.Equals(rule.Mode, "block", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public static string CandidateIdentityKey(Candidate candidate)
|
||||
{
|
||||
var channel = candidate.ChannelSlug.Trim().TrimStart('@').ToLowerInvariant();
|
||||
if (!string.IsNullOrWhiteSpace(channel))
|
||||
{
|
||||
return $"slug:{channel}";
|
||||
}
|
||||
|
||||
return $"name:{candidate.DisplayName.Trim().ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
public static string CandidateIdentityKey(string displayName, string channelSlug)
|
||||
{
|
||||
var channel = channelSlug.Trim().TrimStart('@').ToLowerInvariant();
|
||||
if (!string.IsNullOrWhiteSpace(channel))
|
||||
{
|
||||
return $"slug:{channel}";
|
||||
}
|
||||
|
||||
return $"name:{displayName.Trim().ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
private static WorkflowRuleSetting[] Parse(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<WorkflowRuleSetting[]>(json, JsonOptions) ?? [];
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private static WorkflowRuleSetting Normalize(WorkflowRuleSetting rule, WorkflowRuleSetting fallback)
|
||||
{
|
||||
var mode = rule.Mode.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"block" => "block",
|
||||
"warn" => "warn",
|
||||
_ => fallback.Mode,
|
||||
};
|
||||
|
||||
return rule with
|
||||
{
|
||||
Key = fallback.Key,
|
||||
Label = string.IsNullOrWhiteSpace(rule.Label) ? fallback.Label : rule.Label.Trim(),
|
||||
Enabled = rule.Enabled,
|
||||
Limit = Math.Clamp(rule.Limit, 1, 50),
|
||||
Mode = mode,
|
||||
Description = string.IsNullOrWhiteSpace(rule.Description) ? fallback.Description : rule.Description.Trim(),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user