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' });
|
||||
}
|
||||
|
||||
// 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
|
||||
await db('photos')
|
||||
.where({ id: photoId })
|
||||
.update({ category_id: category_id || null });
|
||||
.update(updateData);
|
||||
|
||||
res.json({ message: 'Photo updated successfully' });
|
||||
} catch (error) {
|
||||
@@ -584,10 +597,18 @@ router.post('/:eventId/photos/bulk-update', adminAuth, async (req, res) => {
|
||||
return res.status(400).json({ error: 'Some photos do not belong to this event' });
|
||||
}
|
||||
|
||||
// Update photos
|
||||
// Prepare update data
|
||||
const updateData = {};
|
||||
if (updates.category_id !== undefined) {
|
||||
updateData.category_id = updates.category_id || null;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
await db('photos')
|
||||
|
||||
@@ -12,8 +12,12 @@ const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '.
|
||||
function resolvePhotoFilePath(event, photo) {
|
||||
if (!event || !photo) throw new Error('resolvePhotoFilePath requires event and photo');
|
||||
|
||||
const mode = (event.source_mode || photo.source_origin || 'managed');
|
||||
if (mode === 'reference' || photo.source_origin === 'external') {
|
||||
// IMPORTANT: photo.source_origin takes precedence over event.source_mode
|
||||
// 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) {
|
||||
throw new Error('Missing external_relpath for external photo');
|
||||
}
|
||||
|
||||
@@ -162,13 +162,26 @@ export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
|
||||
</div>
|
||||
|
||||
{/* 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" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 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) => {
|
||||
const actualIndex = photos.findIndex(p => p.id === photo.id);
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user