Upload attachment from custom widget

A simple custom widget to upload attachment:
https://grist.incubateur.anct.gouv.fr/o/tutos-templates/mfNg44qMCJGm/Ex-envoyer-un-email-avec-PJ/p/3

noctule

Here are the steps to upload an attachment to a table:

  • Retrieve the token
    const tokenInfo = await grist.docApi.getAccessToken({ readOnly: false });

  • Make the API request to load the file (to store it, but it’s not currently linked to a table)

const formData = new FormData();

formData.append('upload', file, file.name);

const response = await fetch(`${tokenInfo.baseUrl}/attachments?auth=${tokenInfo.token}`, {

method: 'POST',

body: formData,

headers: {

"X-Requested-With": "XMLHttpRequest",

},

});

  • Retrieve existing attachments from the cell
    const existingAttachments = currentRecord.PJ || [];

  • Create the list including the new attachment

const newAttachmentsList = Array.isArray(existingAttachments)

? [...existingAttachments, attachmentId]

: [attachmentId];

  • Update the cell with the new attachment

const table = await grist.getTable();

await table.update({
	d: currentRecord.id,
	fields: {
	PJ: ['L', ...newAttachmentsList]
	      }
	});

The full code below:

<!doctype html>
<html lang="fr">
	<head>
		<meta charset="utf-8"/>
		<script src="https://docs.getgrist.com/grist-plugin-api.js"></script>
		<style>
			body {
				font-family: system-ui;
				padding: 20px;
			}
			button, input {
				margin-top: 8px;
			}
			pre {
				background: #f6f8fa;
				padding: 8px;
				border-radius: 6px;
				white-space: pre-wrap;
			}
		</style>
	</head>
	<body>
		<h3>📎 Uploader une PJ</h3>
		<input type="file" id="fileInput" />
		<button id="uploadBtn">Uploader</button>
		<pre id="log">En attente...</pre>

		<script>
			const log = document.getElementById('log');
			const fileInput = document.getElementById('fileInput');
			const uploadBtn = document.getElementById('uploadBtn');

			let currentRecord = null;
			let tableId = null;

			// Initialiser Grist
			grist.ready({ requiredAccess: 'full', columns: [{ name: 'Attachments', type: 'Attachments' }] });

			// Récupérer la ligne sélectionnée
			grist.onRecord((record, mappings) => {
				currentRecord = record;
				log.textContent = `Ligne sélectionnée: id=${record.id}`;
			});

			console.log("uploadBtn", uploadBtn);
			uploadBtn.addEventListener('click', async () => {
		
				const file = fileInput.files[0];

				if (!file) {
					log.textContent = '❌ Sélectionne un fichier';
					return;
				}
				
				if (!currentRecord) {
					log.textContent = '❌ Sélectionne une ligne dans Grist';
					return;
				}

				try {
					log.textContent = '⏳ Upload en cours...';

					// 1. Récupérer un access token (avec droits d'écriture)
					const tokenInfo = await grist.docApi.getAccessToken({ readOnly: false });
					log.textContent += `\n✅ Token obtenu`;

					// 2. Uploader le fichier via l'API REST
					const formData = new FormData();
					formData.append('upload', file, file.name);

					const response = await fetch(`${tokenInfo.baseUrl}/attachments?auth=${tokenInfo.token}`, {
						method: 'POST',
						body: formData,
						headers: {
          		"X-Requested-With": "XMLHttpRequest",
    				},
					});

					if (!response.ok) {
						throw new Error(`Upload échoué: ${response.status} ${response.statusText}`);
					}

					const result = await response.json();
					const attachmentId = result[0];
					log.textContent += `\n✅ Fichier uploadé, id=${attachmentId}`;

					// 3. Récupérer les PJs existantes de la cellule
					const existingAttachments = currentRecord.PJ || [];
					// Grist stocke les attachments sous forme ['L', id1, id2, ...]
					// Mais onRecord les renvoie déjà décodés en array
					const newAttachmentsList = Array.isArray(existingAttachments) 
						? [...existingAttachments, attachmentId]
						: [attachmentId];

					// 4. Mettre à jour la cellule avec la nouvelle PJ
					const table = await grist.getTable();
					await table.update({
						id: currentRecord.id,
						fields: {
							PJ: ['L', ...newAttachmentsList]
						}
					});

					log.textContent += `\n✅ Attachment lié à la ligne ${currentRecord.id}`;
					log.textContent += `\n\n🎉 Terminé !`;

				} catch (err) {
					log.textContent += `\n❌ Erreur: ${err.message}`;
					console.error(err);
				}
			});
		</script>
	</body>
</html>

3 Likes

I love it! Thank you for sharing, Aude.

1 Like