Fix issue #30: Critical bugs in Reference (external folder) mode
This commit fixes the core bugs that prevented Reference mode from functioning:
1. Missing external_relpath Error (CRITICAL FIX)
- Root cause: photoResolver prioritized event.source_mode over photo.source_origin
- Problem: Events in "reference" mode with uploaded photos would fail
because uploaded photos have source_origin='managed' but were being
treated as external photos (requiring external_relpath)
- Fix: Prioritize photo.source_origin over event.source_mode
- Result: Events can now have MIXED sources - imported external photos
AND newly uploaded managed photos coexisting correctly
- File: backend/src/services/photoResolver.js:19
2. Category Assignment Failure (CRITICAL FIX)
- Root cause: Update endpoints modified category_id column but display
used photo.type field ('individual' or 'collage')
- Problem: Category changes appeared to succeed but had no visible effect
- Fix: When category_id is 'individual' or 'collage', update the type
field instead of category_id
- Result: Category assignments now work correctly for all photos
- Files: backend/src/routes/adminPhotos.js:489-497, 605-607
3. Scroll Button Non-Functional (UX FIX)
- Root cause: Scroll indicator was purely visual (no click handler)
- Problem: Users expected to click the animated chevron to scroll
- Fix: Convert div to button with smooth scroll to grid section
- Result: Scroll button now functions as expected with proper a11y
- File: frontend/src/components/gallery/layouts/HeroGalleryLayout.tsx:165-184
Technical Details:
Mixed Source Support:
The photoResolver now correctly handles events that mix:
- External photos: source_origin='external' + external_relpath set
- Uploaded photos: source_origin='managed' + path in storage/events/active
This allows users to start with external media import and later upload
additional photos without errors.
Category/Type Distinction:
The system uses photo.type ('individual'|'collage') for display but also
has a legacy category_id column. The update logic now handles both:
- String values 'individual'/'collage' → update type field
- Numeric values → update legacy category_id field (backward compat)
Notes on Remaining Issues:
Issue #30 also mentioned:
4. Image display (cropped square) - This is by design. Thumbnails use
fit='cover' by default for consistent grid layouts. Can be changed
via app_settings.thumbnail_fit if needed.
5. Theme application - The "Apply Theme" button updates the form state
correctly. Users need to click "Save Changes" to persist to database.
This is standard form behavior, not a bug.
Testing:
- Create event in reference mode with external media
- Upload new photos to the same event → verify no external_relpath error
- Change categories on both external and uploaded photos → verify changes apply
- Use Hero gallery layout → verify scroll button works
Fixes #30
This commit is contained in:
@@ -483,10 +483,23 @@ router.patch('/:eventId/photos/:photoId', adminAuth, async (req, res) => {
|
|||||||
return res.status(404).json({ error: 'Photo not found' });
|
return res.status(404).json({ error: 'Photo not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prepare update data
|
||||||
|
const updateData = {};
|
||||||
|
|
||||||
|
// Handle type-based categories ('individual' or 'collage')
|
||||||
|
// These are string values that map to the photo.type field
|
||||||
|
if (category_id === 'individual' || category_id === 'collage') {
|
||||||
|
updateData.type = category_id;
|
||||||
|
updateData.category_id = null; // Clear legacy category_id
|
||||||
|
} else {
|
||||||
|
// Handle legacy numeric category IDs
|
||||||
|
updateData.category_id = category_id || null;
|
||||||
|
}
|
||||||
|
|
||||||
// Update photo
|
// Update photo
|
||||||
await db('photos')
|
await db('photos')
|
||||||
.where({ id: photoId })
|
.where({ id: photoId })
|
||||||
.update({ category_id: category_id || null });
|
.update(updateData);
|
||||||
|
|
||||||
res.json({ message: 'Photo updated successfully' });
|
res.json({ message: 'Photo updated successfully' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -584,11 +597,19 @@ router.post('/:eventId/photos/bulk-update', adminAuth, async (req, res) => {
|
|||||||
return res.status(400).json({ error: 'Some photos do not belong to this event' });
|
return res.status(400).json({ error: 'Some photos do not belong to this event' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update photos
|
// Prepare update data
|
||||||
const updateData = {};
|
const updateData = {};
|
||||||
if (updates.category_id !== undefined) {
|
if (updates.category_id !== undefined) {
|
||||||
|
// Handle type-based categories ('individual' or 'collage')
|
||||||
|
// These are string values that map to the photo.type field
|
||||||
|
if (updates.category_id === 'individual' || updates.category_id === 'collage') {
|
||||||
|
updateData.type = updates.category_id;
|
||||||
|
updateData.category_id = null; // Clear legacy category_id
|
||||||
|
} else {
|
||||||
|
// Handle legacy numeric category IDs
|
||||||
updateData.category_id = updates.category_id || null;
|
updateData.category_id = updates.category_id || null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await db('photos')
|
await db('photos')
|
||||||
.whereIn('id', photoIds)
|
.whereIn('id', photoIds)
|
||||||
|
|||||||
@@ -12,8 +12,12 @@ const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '.
|
|||||||
function resolvePhotoFilePath(event, photo) {
|
function resolvePhotoFilePath(event, photo) {
|
||||||
if (!event || !photo) throw new Error('resolvePhotoFilePath requires event and photo');
|
if (!event || !photo) throw new Error('resolvePhotoFilePath requires event and photo');
|
||||||
|
|
||||||
const mode = (event.source_mode || photo.source_origin || 'managed');
|
// IMPORTANT: photo.source_origin takes precedence over event.source_mode
|
||||||
if (mode === 'reference' || photo.source_origin === 'external') {
|
// This allows events in "reference" mode to have mixed sources:
|
||||||
|
// - Imported photos: source_origin = 'external'
|
||||||
|
// - Uploaded photos: source_origin = 'managed'
|
||||||
|
const mode = (photo.source_origin || event.source_mode || 'managed');
|
||||||
|
if (mode === 'reference' || mode === 'external') {
|
||||||
if (!photo.external_relpath) {
|
if (!photo.external_relpath) {
|
||||||
throw new Error('Missing external_relpath for external photo');
|
throw new Error('Missing external_relpath for external photo');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,13 +162,26 @@ export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scroll Indicator */}
|
{/* Scroll Indicator */}
|
||||||
<div className="absolute bottom-8 left-1/2 transform -translate-x-1/2 animate-bounce">
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
// Scroll to the grid section
|
||||||
|
const gridSection = document.getElementById('gallery-grid-section');
|
||||||
|
if (gridSection) {
|
||||||
|
gridSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
} else {
|
||||||
|
// Fallback: scroll down by hero section height
|
||||||
|
window.scrollBy({ top: window.innerHeight * 0.9, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="absolute bottom-8 left-1/2 transform -translate-x-1/2 animate-bounce cursor-pointer hover:scale-110 transition-transform focus:outline-none focus:ring-2 focus:ring-white focus:ring-opacity-50 rounded-full p-2"
|
||||||
|
aria-label="Scroll to gallery"
|
||||||
|
>
|
||||||
<ChevronDown className="w-8 h-8 text-white drop-shadow-lg" />
|
<ChevronDown className="w-8 h-8 text-white drop-shadow-lg" />
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Grid Section */}
|
{/* Grid Section */}
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
|
<div id="gallery-grid-section" className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
|
||||||
{remainingPhotos.map((photo) => {
|
{remainingPhotos.map((photo) => {
|
||||||
const actualIndex = photos.findIndex(p => p.id === photo.id);
|
const actualIndex = photos.findIndex(p => p.id === photo.id);
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user