Refine branding labels and access checks

This commit is contained in:
Codex Agent
2026-01-15 08:51:06 +01:00
parent b0ba29fcb6
commit 2b187e7864
5 changed files with 66 additions and 16 deletions

View File

@@ -1925,10 +1925,10 @@
"surface": "Fläche", "surface": "Fläche",
"lockedBranding": "Branding ist in diesem Paket gesperrt.", "lockedBranding": "Branding ist in diesem Paket gesperrt.",
"source": "Branding-Quelle", "source": "Branding-Quelle",
"sourceHint": "Nutze das Tenant-Branding oder überschreibe es für dieses Event.", "sourceHint": "Nutze das Standard-Branding oder passe nur dieses Event an.",
"useDefault": "Tenant", "useDefault": "Standard",
"useCustom": "Event", "useCustom": "Dieses Event",
"usingDefault": "Tenant-Branding aktiv", "usingDefault": "Standard-Branding aktiv",
"usingCustom": "Event-Branding aktiv", "usingCustom": "Event-Branding aktiv",
"mode": "Theme", "mode": "Theme",
"modeLight": "Hell", "modeLight": "Hell",

View File

@@ -1929,10 +1929,10 @@
"surface": "Surface", "surface": "Surface",
"lockedBranding": "Branding is locked for this package.", "lockedBranding": "Branding is locked for this package.",
"source": "Branding source", "source": "Branding source",
"sourceHint": "Use tenant branding or override for this event.", "sourceHint": "Use the default branding or customize this event only.",
"useDefault": "Tenant", "useDefault": "Default",
"useCustom": "Event", "useCustom": "This event",
"usingDefault": "Tenant branding active", "usingDefault": "Default branding active",
"usingCustom": "Event branding active", "usingCustom": "Event branding active",
"mode": "Theme", "mode": "Theme",
"modeLight": "Light", "modeLight": "Light",

View File

@@ -0,0 +1,29 @@
import { describe, expect, it } from 'vitest';
import { isBrandingAllowed, isWatermarkAllowed } from '../events';
describe('event branding access helpers', () => {
it('respects package-level disallow', () => {
const event = {
settings: { branding_allowed: true, watermark_allowed: true },
package: { branding_allowed: false, watermark_allowed: false },
};
expect(isBrandingAllowed(event as any)).toBe(false);
expect(isWatermarkAllowed(event as any)).toBe(false);
});
it('uses settings when package allows', () => {
const event = {
settings: { branding_allowed: false, watermark_allowed: true },
package: { branding_allowed: true, watermark_allowed: true },
};
expect(isBrandingAllowed(event as any)).toBe(false);
expect(isWatermarkAllowed(event as any)).toBe(true);
});
it('defaults to allow when nothing is set', () => {
expect(isBrandingAllowed({} as any)).toBe(true);
expect(isWatermarkAllowed({} as any)).toBe(true);
});
});

View File

@@ -92,7 +92,28 @@ export function resolveEngagementMode(event?: TenantEvent | null): 'tasks' | 'ph
export function isBrandingAllowed(event?: TenantEvent | null): boolean { export function isBrandingAllowed(event?: TenantEvent | null): boolean {
if (!event) return true; if (!event) return true;
return Boolean((event.package as any)?.branding_allowed ?? true); const settings = (event.settings ?? {}) as Record<string, unknown>;
const packageAllowed = (event.package as any)?.branding_allowed;
if (packageAllowed === false) {
return false;
}
if (typeof settings.branding_allowed === 'boolean') {
return settings.branding_allowed;
}
return true;
}
export function isWatermarkAllowed(event?: TenantEvent | null): boolean {
if (!event) return true;
const settings = (event.settings ?? {}) as Record<string, unknown>;
const packageAllowed = (event.package as any)?.watermark_allowed;
if (packageAllowed === false) {
return false;
}
if (typeof settings.watermark_allowed === 'boolean') {
return settings.watermark_allowed;
}
return true;
} }
export function formatEventStatusLabel( export function formatEventStatusLabel(

View File

@@ -10,7 +10,7 @@ import { MobileCard, CTAButton, SkeletonCard } from './components/Primitives';
import { TenantEvent, getEvent, updateEvent, getTenantFonts, getTenantSettings, TenantFont, WatermarkSettings, trackOnboarding } from '../api'; import { TenantEvent, getEvent, updateEvent, getTenantFonts, getTenantSettings, TenantFont, WatermarkSettings, trackOnboarding } from '../api';
import { isAuthError } from '../auth/tokens'; import { isAuthError } from '../auth/tokens';
import { ApiError, getApiErrorMessage } from '../lib/apiError'; import { ApiError, getApiErrorMessage } from '../lib/apiError';
import { isBrandingAllowed } from '../lib/events'; import { isBrandingAllowed, isWatermarkAllowed } from '../lib/events';
import { MobileSheet } from './components/Sheet'; import { MobileSheet } from './components/Sheet';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { adminPath } from '../constants'; import { adminPath } from '../constants';
@@ -179,7 +179,7 @@ export default function MobileBrandingPage() {
const previewLogoUrl = previewForm.logoMode === 'upload' ? previewForm.logoDataUrl : ''; const previewLogoUrl = previewForm.logoMode === 'upload' ? previewForm.logoDataUrl : '';
const previewLogoValue = previewForm.logoMode === 'emoticon' ? previewForm.logoValue : ''; const previewLogoValue = previewForm.logoMode === 'emoticon' ? previewForm.logoValue : '';
const previewInitials = getInitials(previewTitle); const previewInitials = getInitials(previewTitle);
const watermarkAllowed = event?.package?.watermark_allowed !== false; const watermarkAllowed = isWatermarkAllowed(event ?? null);
const brandingAllowed = isBrandingAllowed(event ?? null); const brandingAllowed = isBrandingAllowed(event ?? null);
const watermarkLocked = watermarkAllowed && !brandingAllowed; const watermarkLocked = watermarkAllowed && !brandingAllowed;
const brandingDisabled = !brandingAllowed || form.useDefaultBranding; const brandingDisabled = !brandingAllowed || form.useDefaultBranding;
@@ -616,17 +616,17 @@ export default function MobileBrandingPage() {
{t('events.branding.source', 'Branding Source')} {t('events.branding.source', 'Branding Source')}
</Text> </Text>
<Text fontSize="$sm" color={muted}> <Text fontSize="$sm" color={muted}>
{t('events.branding.sourceHint', 'Nutze das Tenant-Branding oder überschreibe es für dieses Event.')} {t('events.branding.sourceHint', 'Use the default branding or customize this event only.')}
</Text> </Text>
<XStack space="$2"> <XStack space="$2">
<ModeButton <ModeButton
label={t('events.branding.useDefault', 'Tenant')} label={t('events.branding.useDefault', 'Default')}
active={form.useDefaultBranding} active={form.useDefaultBranding}
onPress={() => setForm((prev) => ({ ...prev, useDefaultBranding: true }))} onPress={() => setForm((prev) => ({ ...prev, useDefaultBranding: true }))}
disabled={!brandingAllowed} disabled={!brandingAllowed}
/> />
<ModeButton <ModeButton
label={t('events.branding.useCustom', 'Event')} label={t('events.branding.useCustom', 'This event')}
active={!form.useDefaultBranding} active={!form.useDefaultBranding}
onPress={() => setForm((prev) => ({ ...prev, useDefaultBranding: false }))} onPress={() => setForm((prev) => ({ ...prev, useDefaultBranding: false }))}
disabled={!brandingAllowed} disabled={!brandingAllowed}
@@ -1275,7 +1275,7 @@ function LabeledSlider({
disabled?: boolean; disabled?: boolean;
suffix?: string; suffix?: string;
}) { }) {
const { textStrong, muted } = useAdminTheme(); const { textStrong, muted, primary } = useAdminTheme();
return ( return (
<YStack space="$1.5"> <YStack space="$1.5">
<XStack alignItems="center" justifyContent="space-between"> <XStack alignItems="center" justifyContent="space-between">
@@ -1295,7 +1295,7 @@ function LabeledSlider({
value={value} value={value}
disabled={disabled} disabled={disabled}
onChange={(event) => onChange(Number(event.target.value))} onChange={(event) => onChange(Number(event.target.value))}
style={{ width: '100%' }} style={{ width: '100%', height: 28, accentColor: primary }}
/> />
</YStack> </YStack>
); );