I’m really surprised that there’s no built-in custom widget for viewing attachments. It was easy enough for ChatGPT to make one for me with the Custom Widget Builder, but given the existence of the image viewer widget, it seems like something that should be built-in. Has this been discussed before? Is there a reason there isn’t one?
My thinking exactly. It’s a really obvious deficiency among with, unfortunately, quite a few others where the whole area of “normal, non-technial people seeking to work with Grist” is concerned.
That being said, would you mind sharing what ChatGPT made for you? I’m sure it could be useful to the rest of us, too. ![]()
Sure. Just create a Custom Widget Builder, and put the following in the HTML and the Javascript pages.
<!DOCTYPE html>
<meta charset="utf-8" />
<script src="https://docs.getgrist.com/grist-plugin-api.js"></script>
<style>
:root { color-scheme: light dark; }
html, body { height: 100%; margin: 0; }
body {
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, "Helvetica Neue", Arial, sans-serif;
display: grid; place-items: center;
background: canvas; color: canvastext;
}
#wrap { position: relative; width: 100%; height: 100%; }
#img {
max-width: 100%;
max-height: 100%;
object-fit: cover;
object-position: center;
display: none;
}
</style>
<div id="wrap">
<img id="img" alt="" />
<div id="msg">Select a row and map an attachment column in the widget settings…</div>
</div>
// Request mapping for one Attachments-type column.
grist.ready({
requiredAccess: 'read table',
columns: [{
name: 'Attachment', // logical name used inside the widget
title: 'Attachment column', // label shown in the mapping UI
type: 'Attachments',
optional: false
}]
});
const img = document.getElementById('img');
const msg = document.getElementById('msg');
async function buildAttachmentUrl(attId) {
// Short-lived, safe URL parts for the current document
const { token, baseUrl } = await grist.docApi.getAccessToken({ readOnly: true });
return `${baseUrl}/attachments/${attId}/download?auth=${token}`;
}
function showMessage(text) {
img.removeAttribute('src');
img.style.display = 'none';
msg.textContent = text;
}
async function render(record) {
const mapped = grist.mapColumnNames(record) || {};
const list = mapped.Attachment;
// Attachment cells are arrays of IDs; show the first if present.
const attId = Array.isArray(list) && list.length ? list[0] : null;
if (!attId) {
showMessage('X'); //whatever you want to show if nothing
return;
}
try {
const url = await buildAttachmentUrl(attId);
img.src = url;
img.alt = `Attachment ${attId}`;
img.style.display = 'block';
msg.textContent = '';
} catch (err) {
console.error('Attachment load error:', err);
showMessage('Unable to load image (check access or file type).');
} finally {
// Nudge height after image settles
queueMicrotask(() => grist.setHeight(document.body.scrollHeight));
}
}
// Update when the selected record changes.
grist.onRecord(render);
// Keep height sensible on resizes and image events.
window.addEventListener('resize', () => grist.setHeight(document.body.scrollHeight));
img.addEventListener('load', () => grist.setHeight(document.body.scrollHeight));
img.addEventListener('error', () => grist.setHeight(document.body.scrollHeight));
Once you have this all you need to do is specify the attachment field.
Was having trouble getting this to work, but removed references to grist.setHeight and made sure the widget had full permissions (due to getAccessToken) and it works well!
Amended JS:
// Request mapping for one Attachments-type column.
grist.ready({
requiredAccess: 'read table',
columns: [{
name: 'Attachment', // logical name used inside the widget
title: 'Attachment column', // label shown in the mapping UI
type: 'Attachments',
optional: false
}]
});
const img = document.getElementById('img');
const msg = document.getElementById('msg');
async function buildAttachmentUrl(attId) {
// Short-lived, safe URL parts for the current document
const { token, baseUrl } = await grist.docApi.getAccessToken({ readOnly: true });
return `${baseUrl}/attachments/${attId}/download?auth=${token}`;
}
function showMessage(text) {
img.removeAttribute('src');
img.style.display = 'none';
msg.textContent = text;
}
async function render(record) {
const mapped = grist.mapColumnNames(record) || {};
const list = mapped.Attachment;
// Attachment cells are arrays of IDs; show the first if present.
const attId = Array.isArray(list) && list.length ? list[0] : null;
if (!attId) {
showMessage('X'); //whatever you want to show if nothing
return;
}
try {
const url = await buildAttachmentUrl(attId);
img.src = url;
img.alt = `Attachment ${attId}`;
img.style.display = 'block';
msg.textContent = '';
} catch (err) {
console.error('Attachment load error:', err);
showMessage('Unable to load image (check access or file type).');
}
}
// Update when the selected record changes.
grist.onRecord(render);
Interesting. I pasted this change into a copy of the widget on a different page, and both work identically. I wonder why the AI thought it needed the extra code to do the setHeight stuff if it doesn’t do anything?
Very cool !
In order for people to use this idea more esaily, I created a widget here: GitHub - rambip/grist-attachment-widgets: Small widget to generate a temporary URL from an attachment in Grist
It handles multiple widgets in a column.
I hope this can be useful to some gristers !
This is such a nice tool! Unfortunately it’s not longer working with the latest version of Grist. An updated version would be highly appreciated! We tested it with version 1.7.11 (not working) and 1.7.8 (working perfectly)
Curious. Are using Antonin_P’s widget at this url (https://rambip.github.io/grist-attachment-widgets/viewer)? That seems to work for me on the latest version.
Hi nick,
thanks for the fast response! I tried the solution of Antonin_P and yours and both are not working. We’re using a selfhosted version - maybe that’s the issue? I just tried Antonin_P’s widget on the public site (getgrist) and it worked pretty well. But there’re also some features available that are not yet available in our selfhosted version - so maybe it’s just version 1.7.10 and 1.7.11 in the self-hosted version which is causing trouble.
Hi nick,
solved the problem - was definitely a user error. Changed the configuration of our docker container and now it’s working! Sorry for all the unnecessary fuss!
I am in the same boat. What did you change to get it to work???
So for us it was necessary to put
GRIST_EXPERIMENTAL_PLUGINS: “true”
in the docker compose file.
And for some reasons it’s not working for every browser (this problem is not occurring for the getgrist version, so maybe another env parameter needs to be changed to solve this, but with firefox everything is working fine)