Skip to content

Adds download/upload attachment archive UIs #1551

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 37 commits into
base: main
Choose a base branch
from
Open

Conversation

Spoffy
Copy link
Contributor

@Spoffy Spoffy commented Apr 4, 2025

Context

The API supports:

  • Downloading all attachments from a Grist doc as an archive, for both internally and externally stored attachments.
  • Uploading a .tar archive of attachments, to re-add any missing attachments to the external store.

Neither of these functionalities is exposed via the UI currently.

Proposed solution

This PR adds:

  • An option to the copy menu for downloading attachment archives
  • An option to document settings to upload a .tar attachment archive

Screenshots will be added in the near future, this is currently in draft to allow previews.

Related issues

#1021

Has this been tested?

  • 👍 yes, I added tests to the test suite
  • 💭 no, because this PR is a draft and still needs work
  • 🙅 no, because this is not relevant here
  • 🙋 no, because I need help

Screenshots / Screencasts

image
image
image
image

@Spoffy Spoffy added preview Launch preview deployment of this PR -UX/UI labels Apr 4, 2025
Copy link
Contributor

github-actions bot commented Apr 4, 2025

Deployed commit e4bf0be0b3c39ccb8841cbb8b8f0a4248d9f8449 as https://grist-gristlabs-grist-core-spoffy-archive-ui.fly.dev (until 2025-05-04T22:53:26.681Z)

@paulfitz
Copy link
Member

paulfitz commented Apr 7, 2025

I know this is still a draft but I gave it a try. Basic functionality works for me - some quick notes:

  • Maybe hide "upload missing attachments" if storage is internal?
  • My download was entitled "Untitled document-Attachments.zip" despite the document having a name set.
  • It would be good to remove the "cannot copy a document with external attachments" restriction at this time.

The download UI has clearly had some thought put into it, thanks for that!

Copy link
Contributor

github-actions bot commented Apr 8, 2025

Deployed commit 1ca1afbafa1506c03112ce6cc983f32297e8c6aa as https://grist-gristlabs-grist-core-spoffy-archive-ui.fly.dev (until 2025-05-08T01:26:07.544Z)

@Spoffy Spoffy force-pushed the spoffy/archive-ui branch from 1ca1afb to 714c645 Compare April 12, 2025 13:40
Copy link
Contributor

Deployed commit 714c645b11c76c2f20a48e125328a17e502a425c as https://grist-gristlabs-grist-core-spoffy-archive-ui.fly.dev (until 2025-05-12T13:46:21.053Z)

@Spoffy Spoffy marked this pull request as ready for review April 15, 2025 12:56
Copy link
Contributor

Deployed commit a61d9d28998f7550290f9e677239245a5d84e094 as https://grist-gristlabs-grist-core-spoffy-archive-ui.fly.dev (until 2025-05-15T12:59:16.814Z)

Copy link
Contributor

Deployed commit b2e41c8f7bd7e02b6b7a8d4aa4a259dc0d1a3b89 as https://grist-gristlabs-grist-core-spoffy-archive-ui.fly.dev (until 2025-05-15T13:20:43.504Z)

@Spoffy Spoffy marked this pull request as draft April 15, 2025 20:25
Copy link
Contributor

Deployed commit 414c922ca10abf3b621b8ca2cacf419ef52576a3 as https://grist-gristlabs-grist-core-spoffy-archive-ui.fly.dev (until 2025-05-29T13:17:59.283Z)

@berhalak berhalak self-requested a review April 29, 2025 13:41
Copy link
Contributor

Deployed commit b3062978bc5711778978aa76d36bcd9987f46d1f as https://grist-gristlabs-grist-core-spoffy-archive-ui.fly.dev (until 2025-05-29T17:38:51.014Z)

Copy link
Contributor

Deployed commit e15f0aed7f3a1b3dd46d49f620bc6a7c3826e3fe as https://grist-gristlabs-grist-core-spoffy-archive-ui.fly.dev (until 2025-05-29T17:41:01.180Z)

Copy link
Contributor

Deployed commit 73142445e65217a7ae3e514b75abd2a748562a68 as https://grist-gristlabs-grist-core-spoffy-archive-ui.fly.dev (until 2025-05-29T21:01:52.495Z)

assert.equal(downloadUrl.pathname, idealUrl.pathname, "wrong download link called");
assert.equal(downloadUrl.search, idealUrl.search, "wrong search parameters in url");
// Ensures the page isn't modified / navigated away from by the link, as subsequent tests will fail.
await driver.find('.test-external-attachments-info a').click();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you clicking this link here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was a bug with the link, where it accidentally changes the current page instead of downloading the .tar file.

This clicks it, because if that bug re-occurred then all tests after this one would break.
Although actually, this might be better handled either as a separate test or with additional checks that the current window's URL hasn't changed.

response.data,
fs.createWriteStream(path.join(tmpDownloadsFolder, fileName))
);
assert.include(await fse.readdir(tmpDownloadsFolder), 'attachments.tar', "attachments file wasn't downloaded");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you test first that this file is not there, before downloading it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, can add that!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

if (!file) {
return;
}
isUploadingObs.set(true);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the presence of the comment above, I think it is better to test if the owner is not disposed after promise returns? (Here and below).

Copy link
Contributor Author

@Spoffy Spoffy May 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what you mean, can you describe the change in more detail?

Are you suggesting we check if the owner is disposed before setting the observable? If so, for my benefit learning GrainJS, what's the reason to do that?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are doing an async operation. By the time the promise is resolved, someone could move to another page, destroying all observables you are setting.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've pushed some guards for this. Is this something that Observables should be handling internally?

It feels like once disposed, they should either error or do nothing if set is called on them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know if the additional checks are sufficient!

"If you want to re-import this document, " +
"a separate attachments archive will be needed to restore attachments. "
),
dom('div', cssLink({href: "", target: "_blank"}, t('Learn more.'))),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a stub in common urls?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll go a step further and delete this - this was actually dead code, I just realised. Will double check the rest of the PR too.


// Prevents the div from expanding the parent and makes it only use available space instead.
const cssEagerWrap = styled('div', `
min-width: 100%;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very hacky, didn't know this trick. Wdyt about modern alternative ?
contain: inline-size;

Copy link
Contributor Author

@Spoffy Spoffy May 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oooh, this is nice. Seems to do the same job in practice. Much cleaner than the hack above too.

Weird that this didn't come up when I was searching for options here!

Pushed this fix.

() => downloadAttachmentsModal(doc, pageModel),
menuIcon('Download'),
t('Download attachments...'),
testId('tb-share-option'),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we call this testId in some other way?

Copy link
Contributor Author

@Spoffy Spoffy May 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's convention for everything in this list to have the same test id right now (tb-share-option).

I'm happy to fix that - but it needs to be a separate PR or two, as SaaS uses these test ids too.

@@ -1200,7 +1220,7 @@ export class DocAPIImpl extends BaseAPI implements DocAPI {

public async uploadAttachment(value: string | Blob, filename?: string): Promise<number> {
const formData = this.newFormData();
formData.append('upload', value as Blob, filename);
formData.append('upload', value, filename);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, why we don't need it anymore? (Maybe it was needed in saas build).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FormData objects natively accept string | Blob in .append. I just looked through the Git history, and couldn't see any obvious reason why this was added. Maybe the types accepted by FormData.append changed?

Seems fine now either way!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will check in saas's version of typescript. I remember I was the one added it as the types were complaining.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, let me know if it complains :)

data: formData,
// On browser, it is important not to set Content-Type so that the browser takes care
// of setting HTTP headers appropriately. Outside browser, requestAxios has logic
// for setting the HTTP headers.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have any links to reference?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did some digging, and found a link which describes why this is important.
I updated the comment to this:

// On the browser, Content-Type shouldn't be set as it prevents the browser from setting
// Content-Type with the correct boundary expression to delimit form fields.
// https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest_API/Using_FormData_Objects#sending_files_using_a_formdata_object
// Therefore we omit Content-Type, and allow Axios to handle it as it sees fit - which works
// correctly in the browser and in Node.

* @param {ProgressCB} onProgress - Called periodically during the upload
* @returns {Promise<any>} - Parsed JSON from the endpoint. Uses `any` as no validation is performed.
*/
export async function uploadFormData(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this used anywhere else?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only in 'uploadFiles', which it was extracted from.

It was going to be used elsewhere, but I guess I went a different route.

I'm happy to leave this in though if you are, as it splits up a long function (uploadFiles), making it more readable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine with leaving, maybe just remove the export, to detect dead code more easily in the future.

Copy link
Contributor

github-actions bot commented May 5, 2025

Deployed commit ed65e9a2a7deb7c67df093ed745029989b9ee33d as https://grist-gristlabs-grist-core-spoffy-archive-ui.fly.dev (until 2025-06-04T23:24:10.420Z)

Copy link
Contributor

@berhalak berhalak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a situation like in a screen below, so there are options to download attachemnts, there is a text that attachments are external, but document settings says opposite.

Also, I don't have any attachments whatsoever, should we still show this ui?

image

@Spoffy
Copy link
Contributor Author

Spoffy commented May 6, 2025

Good catch - looks like I didn't set the condition correctly for documents with no attachments. I've fixed that now!

Copy link
Contributor

github-actions bot commented May 6, 2025

Deployed commit f48dd7cc390cdd41ffc8692a09f82ff022f01d5e as https://grist-gristlabs-grist-core-spoffy-archive-ui.fly.dev (until 2025-06-05T14:05:01.143Z)

Copy link
Contributor

github-actions bot commented May 7, 2025

Deployed commit 0407dc4c0fed5c9a74c249afde3d50fc645ff088 as https://grist-gristlabs-grist-core-spoffy-archive-ui.fly.dev (until 2025-06-06T00:23:17.053Z)

@@ -335,5 +426,24 @@ async function waitForNotPresent(fn: () => WebElementPromise) {

const attachmentSection = () => driver.find('.test-admin-panel-item-preferredStorage');

/*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be removed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
-UX/UI Attachments preview Launch preview deployment of this PR
Projects
Status: No status
Development

Successfully merging this pull request may close these issues.

3 participants