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:
@@ -16,6 +16,8 @@ public sealed class AwardsDbContext(DbContextOptions<AwardsDbContext> options) :
|
||||
public DbSet<RiskFlag> RiskFlags => Set<RiskFlag>();
|
||||
public DbSet<AdminAuditEntry> AdminAuditEntries => Set<AdminAuditEntry>();
|
||||
public DbSet<ClipSubmission> ClipSubmissions => Set<ClipSubmission>();
|
||||
public DbSet<ShowactApplication> ShowactApplications => Set<ShowactApplication>();
|
||||
public DbSet<Sponsor> Sponsors => Set<Sponsor>();
|
||||
public DbSet<SiteSettings> SiteSettings => Set<SiteSettings>();
|
||||
public DbSet<TeamMember> TeamMembers => Set<TeamMember>();
|
||||
public DbSet<TeamRolePermission> TeamRolePermissions => Set<TeamRolePermission>();
|
||||
@@ -40,6 +42,8 @@ public sealed class AwardsDbContext(DbContextOptions<AwardsDbContext> options) :
|
||||
entity.Property(item => item.ImprintUrl).HasMaxLength(400);
|
||||
entity.Property(item => item.ContactUrl).HasMaxLength(400);
|
||||
entity.Property(item => item.SponsorsUrl).HasMaxLength(400);
|
||||
entity.Property(item => item.ShowactsUrl).HasMaxLength(400);
|
||||
entity.Property(item => item.ShowactsContent).HasDefaultValue(string.Empty);
|
||||
entity.Property(item => item.DemoLoginEmail).HasMaxLength(180);
|
||||
entity.Property(item => item.DemoLoginPasswordHash).HasMaxLength(120);
|
||||
entity.Property(item => item.DemoLoginPasswordSalt).HasMaxLength(80);
|
||||
@@ -51,6 +55,14 @@ public sealed class AwardsDbContext(DbContextOptions<AwardsDbContext> options) :
|
||||
entity.Property(item => item.TwitchScope).HasMaxLength(300);
|
||||
entity.Property(item => item.MaintenanceTitle).HasMaxLength(120);
|
||||
entity.Property(item => item.MaintenanceMessage).HasMaxLength(600);
|
||||
entity.Property(item => item.WorkflowRulesJson).HasDefaultValue("[]");
|
||||
entity.Property(item => item.NominationLinkBlacklistJson).HasDefaultValue("[]");
|
||||
entity.Property(item => item.ClipSubmissionsEnabled).HasDefaultValue(false);
|
||||
entity.Property(item => item.ClipReviewEnabled).HasDefaultValue(true);
|
||||
entity.Property(item => item.ClipSubmissionDisabledMessage).HasMaxLength(240);
|
||||
entity.Property(item => item.ShowactApplicationsEnabled).HasDefaultValue(false);
|
||||
entity.Property(item => item.ShowactApplicationDisabledMessage).HasMaxLength(240);
|
||||
entity.Property(item => item.SponsorsVisible).HasDefaultValue(true);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<TeamMember>(entity =>
|
||||
@@ -88,6 +100,12 @@ public sealed class AwardsDbContext(DbContextOptions<AwardsDbContext> options) :
|
||||
entity.Property(item => item.DisplayName).HasMaxLength(120);
|
||||
entity.Property(item => item.ChannelSlug).HasMaxLength(120);
|
||||
entity.Property(item => item.Platform).HasMaxLength(40);
|
||||
entity.Property(item => item.AcceptanceStatus).HasMaxLength(30).HasDefaultValue("open");
|
||||
entity.Property(item => item.AcceptanceNote).HasMaxLength(500);
|
||||
entity.Property(item => item.ClipCompilationUrl).HasMaxLength(500);
|
||||
entity.Property(item => item.ClipCompilationTitle).HasMaxLength(200);
|
||||
entity.Property(item => item.ClipCompilationPlatform).HasMaxLength(40);
|
||||
entity.Property(item => item.ClipEmbedStatus).HasMaxLength(30).HasDefaultValue("unchecked");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Nomination>(entity =>
|
||||
@@ -169,6 +187,34 @@ public sealed class AwardsDbContext(DbContextOptions<AwardsDbContext> options) :
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<ShowactApplication>(entity =>
|
||||
{
|
||||
entity.Property(item => item.ArtistName).HasMaxLength(120);
|
||||
entity.Property(item => item.ContactEmail).HasMaxLength(180);
|
||||
entity.Property(item => item.ContactDiscord).HasMaxLength(120);
|
||||
entity.Property(item => item.PlatformUrl).HasMaxLength(500);
|
||||
entity.Property(item => item.PerformanceType).HasMaxLength(80);
|
||||
entity.Property(item => item.Description).HasMaxLength(1000);
|
||||
entity.Property(item => item.TechnicalNotes).HasMaxLength(1000);
|
||||
entity.Property(item => item.ReferenceUrl).HasMaxLength(500);
|
||||
entity.Property(item => item.Status).HasMaxLength(20);
|
||||
entity.Property(item => item.ReviewNote).HasMaxLength(500);
|
||||
entity.Property(item => item.ReviewedByTwitchId).HasMaxLength(120);
|
||||
entity.Property(item => item.CreatedFromIp).HasMaxLength(80);
|
||||
entity.Property(item => item.UserAgent).HasMaxLength(400);
|
||||
entity.HasIndex(item => new { item.SeasonId, item.Status });
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Sponsor>(entity =>
|
||||
{
|
||||
entity.Property(item => item.Name).HasMaxLength(120);
|
||||
entity.Property(item => item.WebsiteUrl).HasMaxLength(500);
|
||||
entity.Property(item => item.LogoUrl).HasMaxLength(500);
|
||||
entity.Property(item => item.Description).HasMaxLength(500);
|
||||
entity.Property(item => item.Tier).HasMaxLength(80);
|
||||
entity.HasIndex(item => new { item.SeasonId, item.IsVisible, item.SortOrder });
|
||||
});
|
||||
|
||||
SeedData.Apply(modelBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,30 @@ public static class OperationalTablesBootstrapper
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "RiskRulesJson" text NOT NULL DEFAULT '[]';
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "WorkflowRulesJson" text NOT NULL DEFAULT '[]';
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "NominationLinkBlacklistJson" text NOT NULL DEFAULT '[]';
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "ClipSubmissionsEnabled" boolean NOT NULL DEFAULT false;
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "ClipReviewEnabled" boolean NOT NULL DEFAULT true;
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "ClipSubmissionDisabledMessage" character varying(240) NOT NULL DEFAULT 'Clip-Einreichungen sind aktuell geschlossen.';
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "ShowactApplicationsEnabled" boolean NOT NULL DEFAULT false;
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "ShowactApplicationDisabledMessage" character varying(240) NOT NULL DEFAULT 'Showact-Bewerbungen sind aktuell geschlossen.';
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "SponsorsVisible" boolean NOT NULL DEFAULT true;
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "TwitchAuthManagedByDatabase" boolean NOT NULL DEFAULT false;
|
||||
|
||||
@@ -40,6 +64,12 @@ public static class OperationalTablesBootstrapper
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "SponsorsContent" text NOT NULL DEFAULT '';
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "ShowactsUrl" character varying(400) NOT NULL DEFAULT '';
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "ShowactsContent" text NOT NULL DEFAULT '';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "RiskFlags" (
|
||||
"Id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
"SeasonId" integer NULL,
|
||||
@@ -121,6 +151,74 @@ public static class OperationalTablesBootstrapper
|
||||
CREATE INDEX IF NOT EXISTS "IX_ClipSubmissions_CandidateId"
|
||||
ON "ClipSubmissions" ("CandidateId");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "ShowactApplications" (
|
||||
"Id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
"SeasonId" integer NOT NULL,
|
||||
"ArtistName" character varying(120) NOT NULL,
|
||||
"ContactEmail" character varying(180) NOT NULL,
|
||||
"ContactDiscord" character varying(120) NOT NULL,
|
||||
"PlatformUrl" character varying(500) NOT NULL,
|
||||
"PerformanceType" character varying(80) NOT NULL,
|
||||
"Description" character varying(1000) NOT NULL,
|
||||
"TechnicalNotes" character varying(1000) NOT NULL,
|
||||
"ReferenceUrl" character varying(500) NOT NULL,
|
||||
"Status" character varying(20) NOT NULL,
|
||||
"ReviewNote" character varying(500) NULL,
|
||||
"ReviewedByTwitchId" character varying(120) NULL,
|
||||
"CreatedFromIp" character varying(80) NOT NULL,
|
||||
"UserAgent" character varying(400) NOT NULL,
|
||||
"CreatedAt" timestamp with time zone NOT NULL,
|
||||
"ReviewedAt" timestamp with time zone NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "IX_ShowactApplications_SeasonId_Status"
|
||||
ON "ShowactApplications" ("SeasonId", "Status");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "Sponsors" (
|
||||
"Id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
"SeasonId" integer NOT NULL,
|
||||
"Name" character varying(120) NOT NULL,
|
||||
"WebsiteUrl" character varying(500) NOT NULL,
|
||||
"LogoUrl" character varying(500) NOT NULL,
|
||||
"Description" character varying(500) NOT NULL,
|
||||
"Tier" character varying(80) NOT NULL,
|
||||
"SortOrder" integer NOT NULL,
|
||||
"IsVisible" boolean NOT NULL,
|
||||
"CreatedAt" timestamp with time zone NOT NULL,
|
||||
"UpdatedAt" timestamp with time zone NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "IX_Sponsors_SeasonId_IsVisible_SortOrder"
|
||||
ON "Sponsors" ("SeasonId", "IsVisible", "SortOrder");
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'FK_ShowactApplications_Seasons_SeasonId'
|
||||
) THEN
|
||||
ALTER TABLE "ShowactApplications"
|
||||
ADD CONSTRAINT "FK_ShowactApplications_Seasons_SeasonId"
|
||||
FOREIGN KEY ("SeasonId") REFERENCES "Seasons" ("Id")
|
||||
ON DELETE CASCADE;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'FK_Sponsors_Seasons_SeasonId'
|
||||
) THEN
|
||||
ALTER TABLE "Sponsors"
|
||||
ADD CONSTRAINT "FK_Sponsors_Seasons_SeasonId"
|
||||
FOREIGN KEY ("SeasonId") REFERENCES "Seasons" ("Id")
|
||||
ON DELETE CASCADE;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
@@ -138,6 +236,24 @@ public static class OperationalTablesBootstrapper
|
||||
ALTER TABLE "Nominations"
|
||||
ADD COLUMN IF NOT EXISTS "StreamUrl" character varying(300) NULL;
|
||||
|
||||
ALTER TABLE "Candidates"
|
||||
ADD COLUMN IF NOT EXISTS "AcceptanceStatus" character varying(30) NOT NULL DEFAULT 'open';
|
||||
|
||||
ALTER TABLE "Candidates"
|
||||
ADD COLUMN IF NOT EXISTS "AcceptanceNote" character varying(500) NULL;
|
||||
|
||||
ALTER TABLE "Candidates"
|
||||
ADD COLUMN IF NOT EXISTS "ClipCompilationUrl" character varying(500) NULL;
|
||||
|
||||
ALTER TABLE "Candidates"
|
||||
ADD COLUMN IF NOT EXISTS "ClipCompilationTitle" character varying(200) NULL;
|
||||
|
||||
ALTER TABLE "Candidates"
|
||||
ADD COLUMN IF NOT EXISTS "ClipCompilationPlatform" character varying(40) NULL;
|
||||
|
||||
ALTER TABLE "Candidates"
|
||||
ADD COLUMN IF NOT EXISTS "ClipEmbedStatus" character varying(30) NOT NULL DEFAULT 'unchecked';
|
||||
|
||||
ALTER TABLE "Nominations"
|
||||
ADD COLUMN IF NOT EXISTS "Status" character varying(20) NOT NULL DEFAULT 'pending';
|
||||
|
||||
|
||||
@@ -48,7 +48,17 @@ Keine Weitergabe zu Werbezwecken. Technische Dienstleister verarbeiten Daten aus
|
||||
ContactContent = SeedCatalog.DefaultContactContent,
|
||||
SponsorsUrl = "https://vtuber-star-awards.de/partner",
|
||||
SponsorsContent = SeedCatalog.DefaultSponsorsContent,
|
||||
ShowactsUrl = "https://vtuber-star-awards.de/showacts",
|
||||
ShowactsContent = "Informationen zu Showact-Bewerbungen, Ablauf und Kontakt fuer die Award-Show.",
|
||||
RiskRulesJson = RiskRuleSettings.Serialize(RiskRuleSettings.Defaults),
|
||||
WorkflowRulesJson = WorkflowRuleSettings.Serialize(WorkflowRuleSettings.Defaults),
|
||||
NominationLinkBlacklistJson = NominationLinkBlacklistSettings.Serialize(NominationLinkBlacklistSettings.Defaults),
|
||||
ClipSubmissionsEnabled = false,
|
||||
ClipReviewEnabled = true,
|
||||
ClipSubmissionDisabledMessage = "Clip-Einreichungen sind aktuell geschlossen.",
|
||||
ShowactApplicationsEnabled = false,
|
||||
ShowactApplicationDisabledMessage = "Showact-Bewerbungen sind aktuell geschlossen.",
|
||||
SponsorsVisible = true,
|
||||
SocialLinksJson = JsonSerializer.Serialize(new[]
|
||||
{
|
||||
new { label = "Twitch", platform = "twitch", url = "https://twitch.tv/jayuhime", icon = "twitch", showOnHost = true, showOnCommunity = true },
|
||||
|
||||
@@ -41,6 +41,26 @@ public static partial class SeedDataBootstrapper
|
||||
settings.RiskRulesJson = RiskRuleSettings.Serialize(RiskRuleSettings.Defaults);
|
||||
}
|
||||
|
||||
if (!HasValidWorkflowRules(settings.WorkflowRulesJson))
|
||||
{
|
||||
settings.WorkflowRulesJson = WorkflowRuleSettings.Serialize(WorkflowRuleSettings.Defaults);
|
||||
}
|
||||
|
||||
if (!HasValidNominationLinkBlacklist(settings.NominationLinkBlacklistJson))
|
||||
{
|
||||
settings.NominationLinkBlacklistJson = NominationLinkBlacklistSettings.Serialize(NominationLinkBlacklistSettings.Defaults);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(settings.ClipSubmissionDisabledMessage))
|
||||
{
|
||||
settings.ClipSubmissionDisabledMessage = "Clip-Einreichungen sind aktuell geschlossen.";
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(settings.ShowactApplicationDisabledMessage))
|
||||
{
|
||||
settings.ShowactApplicationDisabledMessage = "Showact-Bewerbungen sind aktuell geschlossen.";
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(settings.ImprintContent))
|
||||
{
|
||||
settings.ImprintContent = SeedCatalog.DefaultImprintContent;
|
||||
@@ -55,6 +75,16 @@ public static partial class SeedDataBootstrapper
|
||||
{
|
||||
settings.SponsorsContent = SeedCatalog.DefaultSponsorsContent;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(settings.ShowactsUrl))
|
||||
{
|
||||
settings.ShowactsUrl = "https://vtuber-star-awards.de/showacts";
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(settings.ShowactsContent))
|
||||
{
|
||||
settings.ShowactsContent = "Informationen zu Showact-Bewerbungen, Ablauf und Kontakt fuer die Award-Show.";
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasValidSiteArray(string? json, params string[] requiredKeys)
|
||||
@@ -87,4 +117,30 @@ public static partial class SeedDataBootstrapper
|
||||
|
||||
private static bool HasValidRiskRules(string? json) =>
|
||||
RiskRuleSettings.Read(new Backend.Domain.SiteSettings { RiskRulesJson = json ?? string.Empty }).Length == RiskRuleSettings.Defaults.Length;
|
||||
|
||||
private static bool HasValidWorkflowRules(string? json) =>
|
||||
WorkflowRuleSettings.Read(new Backend.Domain.SiteSettings { WorkflowRulesJson = json ?? string.Empty }).Length == WorkflowRuleSettings.Defaults.Length;
|
||||
|
||||
private static bool HasValidNominationLinkBlacklist(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(json);
|
||||
return document.RootElement.ValueKind == JsonValueKind.Array
|
||||
&& document.RootElement.EnumerateArray().Any(item =>
|
||||
item.ValueKind == JsonValueKind.Object
|
||||
&& item.TryGetProperty("Url", out var url)
|
||||
&& url.ValueKind == JsonValueKind.String
|
||||
&& !string.IsNullOrWhiteSpace(url.GetString()));
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user