fix: address bugs and feature requests from discussion #317
- Share link: display and copy now use the absolute URL built from the current origin instead of the relative path stored in events.share_link. Added a Copy Link button to the events list (inline + dropdown). - Detect dev tools default: event creation now reads the global enable_devtools_protection app setting instead of always falling back to the column default; admins who disable it globally get new events with it disabled too. - Require password default: added a global "Require password by default" setting (event_default_require_password, default true), exposed via Settings -> Events. Create-event form initialises from it. - Filter bar: added gallery_show_filter_bar setting and hide the search/ sort row in the public gallery when off, or when the gallery has zero photos (fixes the empty-state UX from the screenshot). - Theme picker unclickable on Create Event: memoised availableEventTypes so its identity is stable. The "auto-apply event-type recommended preset" effect was firing on every render due to the unstable array reference and silently overwriting the user's preset selection ~1ms after each click. - Branding logo disappearing on theme change: handlePresetChange and handleThemeChange no longer wipe the existing logoUrl when a preset config (which carries no logoUrl) is applied; handleSave falls back to brandingSettings.logo_url. themeMutation now invalidates the admin-settings and public-settings caches so saved theme changes appear immediately.
This commit is contained in:
@@ -113,6 +113,31 @@ const getEventFieldRequirements = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to read app_settings booleans by key, used to inherit per-setting
|
||||
// defaults onto new events. Returns `undefined` for missing/non-boolean rows
|
||||
// so callers can fall back to a legacy default.
|
||||
const readBooleanSetting = async (key) => {
|
||||
try {
|
||||
const setting = await db('app_settings').where('setting_key', key).first();
|
||||
if (!setting) return undefined;
|
||||
let value = setting.setting_value;
|
||||
if (typeof value === 'string') {
|
||||
try { value = JSON.parse(value); } catch { /* keep raw */ }
|
||||
}
|
||||
return typeof value === 'boolean' ? value : undefined;
|
||||
} catch (error) {
|
||||
logger.error('Failed to read app setting', { key, error: error.message });
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to read the global "enable_devtools_protection" admin setting so
|
||||
// new events inherit it instead of always falling back to the DB column default
|
||||
// (#317 — admin disabled it globally but new events still got it ON).
|
||||
const getDownloadProtectionDefaults = async () => {
|
||||
return { enable_devtools_protection: await readBooleanSetting('enable_devtools_protection') };
|
||||
};
|
||||
|
||||
// Helper to get branding defaults for new events (Feature 7: Branding Inheritance)
|
||||
const getBrandingDefaults = async () => {
|
||||
try {
|
||||
@@ -246,6 +271,7 @@ router.post('/', adminAuth, requirePermission('events.create'), [
|
||||
body('upload_category_id').optional({ nullable: true, checkFalsy: true }).isInt(),
|
||||
body('allow_downloads').optional().isBoolean(),
|
||||
body('disable_right_click').optional().isBoolean(),
|
||||
body('enable_devtools_protection').optional().isBoolean(),
|
||||
body('watermark_downloads').optional().isBoolean(),
|
||||
body('watermark_text').optional().trim(),
|
||||
body('css_template_id').optional({ nullable: true, checkFalsy: true }).isInt(),
|
||||
@@ -291,9 +317,10 @@ router.post('/', adminAuth, requirePermission('events.create'), [
|
||||
upload_category_id = null,
|
||||
allow_downloads = true,
|
||||
disable_right_click = false,
|
||||
enable_devtools_protection: enableDevtoolsProtectionInput,
|
||||
watermark_downloads = false,
|
||||
watermark_text = null,
|
||||
require_password: requirePasswordInput = true,
|
||||
require_password: requirePasswordInput,
|
||||
// Feedback settings
|
||||
feedback_enabled = false,
|
||||
allow_ratings = true,
|
||||
@@ -349,7 +376,14 @@ router.post('/', adminAuth, requirePermission('events.create'), [
|
||||
return res.status(400).json({ errors: validationErrors });
|
||||
}
|
||||
|
||||
const requirePassword = parseBooleanInput(requirePasswordInput, true);
|
||||
// Default require_password from global "event_default_require_password"
|
||||
// setting when the body omits it (#317 — admins want to flip the default).
|
||||
let requirePasswordFallback = true;
|
||||
if (requirePasswordInput === undefined) {
|
||||
const setting = await readBooleanSetting('event_default_require_password');
|
||||
if (setting !== undefined) requirePasswordFallback = setting;
|
||||
}
|
||||
const requirePassword = parseBooleanInput(requirePasswordInput, requirePasswordFallback);
|
||||
|
||||
// Debug logging
|
||||
logger.debug('Download control values', {
|
||||
@@ -457,6 +491,17 @@ router.post('/', adminAuth, requirePermission('events.create'), [
|
||||
const effectiveHeroLogoSize = req.body.hero_logo_size || brandingDefaults.hero_logo_size;
|
||||
const effectiveHeroLogoPosition = req.body.hero_logo_position || brandingDefaults.hero_logo_position;
|
||||
|
||||
// Inherit "Detect dev tools" from the global Image Security setting unless
|
||||
// the request explicitly overrides it (#317 — admin disabled it globally
|
||||
// but new events still got it ON because the column default is true).
|
||||
const protectionDefaults = await getDownloadProtectionDefaults();
|
||||
const effectiveEnableDevtoolsProtection =
|
||||
enableDevtoolsProtectionInput !== undefined
|
||||
? enableDevtoolsProtectionInput
|
||||
: protectionDefaults.enable_devtools_protection !== undefined
|
||||
? protectionDefaults.enable_devtools_protection
|
||||
: true;
|
||||
|
||||
// Insert into database
|
||||
const insertResult = await db('events').insert({
|
||||
slug,
|
||||
@@ -479,6 +524,7 @@ router.post('/', adminAuth, requirePermission('events.create'), [
|
||||
upload_category_id,
|
||||
allow_downloads: formatBoolean(allow_downloads !== undefined ? allow_downloads : true),
|
||||
disable_right_click: formatBoolean(disable_right_click !== undefined ? disable_right_click : false),
|
||||
enable_devtools_protection: formatBoolean(effectiveEnableDevtoolsProtection),
|
||||
watermark_downloads: formatBoolean(watermark_downloads !== undefined ? watermark_downloads : false),
|
||||
watermark_text,
|
||||
require_password: formatBoolean(requirePassword),
|
||||
|
||||
@@ -13,7 +13,11 @@ router.get('/', async (req, res) => {
|
||||
this.whereIn('setting_type', ['branding', 'theme', 'general', 'security', 'analytics', 'boolean'])
|
||||
.orWhere('setting_key', 'like', 'analytics_%')
|
||||
.orWhere('setting_key', 'like', 'event_require_%')
|
||||
.orWhereIn('setting_key', ['seo_meta_noindex', 'seo_meta_nofollow', 'seo_meta_noai']);
|
||||
.orWhereIn('setting_key', [
|
||||
'seo_meta_noindex', 'seo_meta_nofollow', 'seo_meta_noai',
|
||||
'event_default_require_password',
|
||||
'gallery_show_filter_bar'
|
||||
]);
|
||||
})
|
||||
.select('setting_key', 'setting_value');
|
||||
});
|
||||
@@ -78,6 +82,10 @@ router.get('/', async (req, res) => {
|
||||
event_require_admin_email: settingsObject.event_require_admin_email !== false,
|
||||
event_require_event_date: settingsObject.event_require_event_date !== false,
|
||||
event_require_expiration: settingsObject.event_require_expiration !== false,
|
||||
// Default value for "Require password" toggle in event creation form
|
||||
event_default_require_password: settingsObject.event_default_require_password !== false,
|
||||
// Whether to show the search/sort filter bar in public galleries (default: true)
|
||||
gallery_show_filter_bar: settingsObject.gallery_show_filter_bar !== false,
|
||||
// Upload settings (safe to expose - needed for client-side validation)
|
||||
allowed_file_types: settingsObject.general_allowed_file_types || 'jpg,jpeg,png,webp',
|
||||
// SEO meta tag flags (safe to expose - these are intended for crawlers)
|
||||
|
||||
Reference in New Issue
Block a user