More than one image appear in user profile (list-details page) for a user

Hi Softr…

I’m a new user and I think (maybe) there is a issue/bug with the list-details page I’m using for a user profile.

I have an edit button to update the user’s image. Somehow the user can add more than one image and it shows up under their profile.

The intent is to update the current profile image. Not have multiple images

Is this a bug or am I doing something incorrect?

Thanks,

AHA

AHA

Many of my app users frequently requested support when they had difficulty uploading their profile photo, whether on mobile or desktop. There are a few issues:

  • The primary issue is interface-related, particularly because the native (X) button to remove your profile picture is quite small and out of frame with the Profile Photo.
  • Most users were attempting to just drag new photos over the top of their old photo.
  • Multiple File attachments allows for the issue of redundant images above, and if the user doesn’t click the small Trashbin, a newly uploaded profile will not replace the old one.
  • The User Profile does not natively refresh after Photo upload, confusing the user as they don’t see immediate success in their profile photo updating

I run a solution on my Profile pages that…truthfully, is a pretty dirty hack, but it gets the job done and helps clear up my user confusion (and lessen my inbox!).

  1. My profile pages are set up with a list-details1 list filtered to the logged-in UserID. This enables me to show their profile photo, details, etc.
  2. In list-details1, I have an Action button (Edit Record) labeled “Update Profile Photo”. Inside the action is only one field to update (the Profile image file field). I use its specific “Update Profile Photo” label as a DOM watcher in the script below.
  3. Below list-details1 is the #useraccounts1 Profile form block. However, I removed the confusing attachment/image upload field from the form.
  4. The two scripts below are responsible for 1) refreshing list-details1 when the Profile form is updated (thus giving the user instant feedback that their photo has been updated), and 2)…here’s the real hacky part…it watches for the Update Profile Photo action from #list-details1 and, when clicked, removes any original Profile Photo (file attachment) from the modal.

The result is a much more friendly-user means of updating Profile Photos.

Scripts below!

***Additionally, I run an Airtable automation that watches for updates on User table records (specifically, the profile Photo field). When updated, it runs a script to assure that only the latest photo uploaded is used (it deletes any older/additional photos). This might help you with the issue of multiple profile photo uploads, which the file upload interface in Softr cannot natively prevent.

PROFILE REFRESH SCRIPT:

// Function to handle the logic for updating list members and default reviewers
function handleUpdateRecordSuccess() {
window.dispatchEvent(new CustomEvent(‘reload-block-list-details1’));
}

// Function to setup the MutationObserver on the target node
function setupMutationObserver() {
const targetNode = document.querySelector(‘#user-accounts1’);

if (targetNode) {
// Options for the observer (which mutations to observe)
const config = { attributes: false, childList: true, subtree: true };

// Callback function to execute when mutations are observed
const callback = function(mutationsList, observer) {
for (const mutation of mutationsList) {
if (mutation.type === ‘childList’) {
mutation.addedNodes.forEach(node => {
// Check if the added node has the ‘success-icon’ class
if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains(‘success-icon’)) {
handleUpdateRecordSuccess();
}
});
}
}
};

// Create an instance of MutationObserver
const observer = new MutationObserver(callback);

// Start observing the target node for configured mutations
observer.observe(targetNode, config);
}
}

// Retry logic to find the target node
let attempts = 0;
const maxAttempts = 5;

function trySetupObserver() {
if (!document.querySelector(‘#user-accounts1’) && attempts < maxAttempts) { setTimeout(trySetupObserver, 300); // Retry after 300ms attempts++; } else if (attempts < maxAttempts) { setupMutationObserver(); } } // Start the first attempt document.addEventListener(‘DOMContentLoaded’, trySetupObserver); MODAL EDITOR SCRIPT: document.addEventListener(‘DOMContentLoaded’, function() { // Flag to track if the click has already been performed let clickPerformed=false; // Function to observe DOM changes const observeDOM=(function() { const MutationObserver=window.MutationObserver || window.WebKitMutationObserver; return function(obj, callback) { if (!obj || obj.nodeType !==1) return; if (MutationObserver) { const mutationObserver=new MutationObserver(callback); mutationObserver.observe(obj, { childList: true, subtree: true }); return mutationObserver; } // Old browsers fallback else if (window.addEventListener) { obj.addEventListener(‘DOMNodeInserted’, callback, false); obj.addEventListener(‘DOMNodeRemoved’, callback, false); } }; })(); // Function to click the second SVG element function clickTrashcan(retries=5) { const formElement=document.querySelector(‘.MuiDialog-paper.css-gimhg7’); const svgElements=formElement ? formElement.querySelectorAll(‘svg’) : null; const trashcanSVG=svgElements && svgElements.length> 1 ? svgElements[1] : null; // Select the second SVG

if (trashcanSVG) {
console.log(‘Clicking trashcan SVG to delete the original image.’);

// Create a new mouse event and dispatch it
const clickEvent = new MouseEvent(‘click’, {
bubbles: true,
cancelable: true,
view: window
});
trashcanSVG.dispatchEvent(clickEvent);

// Mark the click as performed
clickPerformed = true;

// Double check if the click was successful
setTimeout(() => {
const stillPresentTrashcanSVG = formElement.querySelectorAll(‘svg’)[1];
if (stillPresentTrashcanSVG) {
console.log(‘Trashcan SVG still present, retrying…’);
if (retries > 0) {
setTimeout(() => clickTrashcan(retries - 1), 1000); // Retry after 1000ms
} else {
console.log(‘Trashcan SVG not removed after multiple attempts.’);
}
} else {
console.log(‘Trashcan SVG successfully clicked and removed.’);
}
}, 1000); // Check after 1000ms to see if the SVG was removed
} else if (retries > 0) {
console.log(‘Trashcan SVG not found. Retrying…’);
setTimeout(() => clickTrashcan(retries - 1), 1000); // Retry after 1000ms
} else {
console.log(‘Trashcan SVG not found after multiple attempts.’);
}
}

// Function to check if the form is present and its content is loaded
function checkFormPresence() {
const formElement = document.querySelector(‘.MuiDialog-paper.css-gimhg7’);
const photoInput = document.querySelector(‘input[name=“Photo”]’); // Check for an element that should be inside the form
if (formElement && photoInput && !clickPerformed) {
console.log(‘Form and content are present. Attempting to click trashcan button.’);
// Start clicking with retries
setTimeout(() => clickTrashcan(), 500); // Initial delay to ensure the form content is loaded
}
}

// Function to reset clickPerformed flag when “Update Profile Photo” disappears
function resetFlagOnClose() {
const updateProfileText = document.querySelector(‘.MuiBox-root.css-hf1f5m’);
if (!updateProfileText) {
console.log(‘“Update Profile Photo” text not found. Resetting clickPerformed flag.’);
clickPerformed = false;
}
}

// Observe the entire document for changes
observeDOM(document.body, function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.addedNodes && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === 1) {
checkFormPresence();
}
});
}

// Reset clickPerformed flag when the “Update Profile Photo” text disappears
if (mutation.removedNodes && mutation.removedNodes.length > 0) {
mutation.removedNodes.forEach(function(node) {
if (node.nodeType === 1) {
resetFlagOnClose();
}
});
}
});
});

// Initial check in case the form is already present
checkFormPresence();
});

AIRTABLE AUTOMATION SCRIPT // Deletes older uploads in case of redundancy

// Script to keep only the latest image in the ‘Photo’ field
let inputConfig = input.config();
let table = base.getTable(“Users”); // Replace with your table name
let recordId = inputConfig.recordId;
let photoField = “Photo”; // Replace with your photo field name

// Fetch the record
let record = await table.selectRecordAsync(recordId);

if (record) {
let photos = record.getCellValue(photoField);

// Check if there are multiple photos
if (photos && photos.length > 1) {
    // Keep only the latest photo (assuming the latest is the last in the array)
    let latestPhoto = [photos[photos.length - 1]];

    // Update the record with only the latest photo
    await table.updateRecordAsync(recordId, {
        [photoField]: latestPhoto
    });

    console.log(`Updated record ${recordId} to keep only the latest photo.`);
} else {
    console.log(`No update needed for record ${recordId}.`);
}

} else {
console.log(Record with ID ${recordId} not found.);
}

Hi Seth,

Instead of using such hard hack, I may have an easier hack:
why not having the profile picture in the user account block, using an image softr field + small css code to increase the size of the “x” button?

Here would be the code to be inserted in the header - #user-accounts1 to be replaced by your actual account block Id:

<style>
#user-accounts1 .delete-file {
    transform: scale(2); /* replace (2) by whatever you need for the size. Can be (1.5), (3)... as you wish*/
    transform-origin: center;
}
</style>

Or

<style>
#user-accounts1 .static-image {
    position: relative;
}

#user-accounts1 .delete-file {
    transform: scale(2); /* replace (2) by whatever you need for the size. Can be (1.5), (3)... as you wish*/
    transform-origin: top right;
    position: absolute; 
    top: 0; 
    right: 0;
}
</style>

.delete-file is the class for the “x” icon button in the image field for the user profile block.

In addition, if you need to change the color of the “x” button add this in one of the css code:

 background-color: black !important;
 color: white !important; 
 opacity: 1 !important;

The !important is mandatory when dealing with colors of Softr pre-built elements

If you want to center the “x” button inside the profile picture:

<style>
#user-accounts1 .static-image {
    position: relative;
}

#user-accounts1 .delete-file {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, 280%) scale(3);
    transform-origin: center;
    background-color: black !important;
    color: white !important; 
    opacity: 1 !important;
}
</style>

Results with a bigger “x” button, made black:



Results with a bigger “x” button, made black and centered within the profile picture:

Hi Matthieu, thanks for sharing! It’s indeed a better way.
I wonder if you know why my block doesn’t show the preview of avatar image? I already applied the custom code to the header.

Hi Kathy,

You need to replace the avatar field type. Here, you use a file type. The best is to use an image type, see below.

ah, Thanks so much! It worked!! :smiley: