Attachment viewer widget needed

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?

1 Like

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. :slight_smile:

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.

2 Likes

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 !

2 Likes