Feat: add title query params for exported csv and xlsx + download translations (#872)

Co-authored-by: Florent <florent.git@zeteo.me>
This commit is contained in:
CamilleLegeron 2024-03-06 18:12:42 +01:00 committed by GitHub
parent 011cf9da0d
commit 9ce8ed3f25
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 36 additions and 24 deletions

View File

@ -85,7 +85,7 @@ export const GristTooltips: Record<Tooltip, TooltipContentFunc> = {
t('Try out changes in a copy, then decide whether to replace the original with your edits.') t('Try out changes in a copy, then decide whether to replace the original with your edits.')
), ),
dom('div', dom('div',
cssLink({href: commonUrls.helpTryingOutChanges, target: '_blank'}, 'Learn more.'), cssLink({href: commonUrls.helpTryingOutChanges, target: '_blank'}, t('Learn more.')),
), ),
...args, ...args,
), ),

View File

@ -303,7 +303,7 @@ export function downloadDocModal(doc: Document, pageModel: DocPageModel) {
const selected = Observable.create<DownloadOption>(owner, 'full'); const selected = Observable.create<DownloadOption>(owner, 'full');
return [ return [
cssModalTitle(`Download document`), cssModalTitle(t(`Download document`)),
cssRadioCheckboxOptions( cssRadioCheckboxOptions(
radioCheckboxOption(selected, 'full', t("Download full document and history")), radioCheckboxOption(selected, 'full', t("Download full document and history")),
radioCheckboxOption(selected, 'nohistory', t("Remove document history (can significantly reduce file size)")), radioCheckboxOption(selected, 'nohistory', t("Remove document history (can significantly reduce file size)")),
@ -311,7 +311,7 @@ export function downloadDocModal(doc: Document, pageModel: DocPageModel) {
), ),
cssModalButtons( cssModalButtons(
dom.domComputed(use => dom.domComputed(use =>
bigPrimaryButtonLink(`Download`, hooks.maybeModifyLinkAttrs({ bigPrimaryButtonLink(t(`Download`), hooks.maybeModifyLinkAttrs({
href: pageModel.appModel.api.getDocAPI(doc.id).getDownloadUrl({ href: pageModel.appModel.api.getDocAPI(doc.id).getDownloadUrl({
template: use(selected) === "template", template: use(selected) === "template",
removeHistory: use(selected) === "nohistory" || use(selected) === "template", removeHistory: use(selected) === "nohistory" || use(selected) === "template",
@ -325,7 +325,7 @@ export function downloadDocModal(doc: Document, pageModel: DocPageModel) {
testId('download-button-link'), testId('download-button-link'),
), ),
), ),
bigBasicButton('Cancel', dom.on('click', () => { bigBasicButton(t('Cancel'), dom.on('click', () => {
ctl.close(); ctl.close();
})) }))
) )

View File

@ -93,6 +93,7 @@ import * as path from 'path';
import * as t from "ts-interface-checker"; import * as t from "ts-interface-checker";
import {Checker} from "ts-interface-checker"; import {Checker} from "ts-interface-checker";
import uuidv4 from "uuid/v4"; import uuidv4 from "uuid/v4";
import { Document } from "app/gen-server/entity/Document";
// Cap on the number of requests that can be outstanding on a single document via the // Cap on the number of requests that can be outstanding on a single document via the
// rest doc api. When this limit is exceeded, incoming requests receive an immediate // rest doc api. When this limit is exceeded, incoming requests receive an immediate
@ -646,6 +647,9 @@ export class DocWorkerApi {
// full document. // full document.
const dryRun = isAffirmative(req.query.dryrun || req.query.dryRun); const dryRun = isAffirmative(req.query.dryrun || req.query.dryRun);
const dryRunSuccess = () => res.status(200).json({dryRun: 'allowed'}); const dryRunSuccess = () => res.status(200).json({dryRun: 'allowed'});
const filename = await this._getDownloadFilename(req);
// We want to be have a way download broken docs that ActiveDoc may not be able // We want to be have a way download broken docs that ActiveDoc may not be able
// to load. So, if the user owns the document, we unconditionally let them // to load. So, if the user owns the document, we unconditionally let them
// download. // download.
@ -655,13 +659,13 @@ export class DocWorkerApi {
// We carefully avoid creating an ActiveDoc for the document being downloaded, // We carefully avoid creating an ActiveDoc for the document being downloaded,
// in case it is broken in some way. It is convenient to be able to download // in case it is broken in some way. It is convenient to be able to download
// broken files for diagnosis/recovery. // broken files for diagnosis/recovery.
return await this._docWorker.downloadDoc(req, res, this._docManager.storageManager); return await this._docWorker.downloadDoc(req, res, this._docManager.storageManager, filename);
} catch (e) { } catch (e) {
if (e.message && e.message.match(/does not exist yet/)) { if (e.message && e.message.match(/does not exist yet/)) {
// The document has never been seen on file system / s3. It may be new, so // The document has never been seen on file system / s3. It may be new, so
// we try again after having created an ActiveDoc for the document. // we try again after having created an ActiveDoc for the document.
await this._getActiveDoc(req); await this._getActiveDoc(req);
return this._docWorker.downloadDoc(req, res, this._docManager.storageManager); return this._docWorker.downloadDoc(req, res, this._docManager.storageManager, filename);
} else { } else {
throw e; throw e;
} }
@ -674,7 +678,7 @@ export class DocWorkerApi {
throw new ApiError('not authorized to download this document', 403); throw new ApiError('not authorized to download this document', 403);
} }
if (dryRun) { dryRunSuccess(); return; } if (dryRun) { dryRunSuccess(); return; }
return this._docWorker.downloadDoc(req, res, this._docManager.storageManager); return this._docWorker.downloadDoc(req, res, this._docManager.storageManager, filename);
} }
})); }));
@ -1222,7 +1226,7 @@ export class DocWorkerApi {
this._app.get('/api/docs/:docId/download/table-schema', canView, withDoc(async (activeDoc, req, res) => { this._app.get('/api/docs/:docId/download/table-schema', canView, withDoc(async (activeDoc, req, res) => {
const doc = await this._dbManager.getDoc(req); const doc = await this._dbManager.getDoc(req);
const options = this._getDownloadOptions(req, doc.name); const options = await this._getDownloadOptions(req, doc);
const tableSchema = await collectTableSchemaInFrictionlessFormat(activeDoc, req, options); const tableSchema = await collectTableSchemaInFrictionlessFormat(activeDoc, req, options);
const apiPath = await this._grist.getResourceUrl(doc, 'api'); const apiPath = await this._grist.getResourceUrl(doc, 'api');
const query = new URLSearchParams(req.query as {[key: string]: string}); const query = new URLSearchParams(req.query as {[key: string]: string});
@ -1241,18 +1245,16 @@ export class DocWorkerApi {
})); }));
this._app.get('/api/docs/:docId/download/csv', canView, withDoc(async (activeDoc, req, res) => { this._app.get('/api/docs/:docId/download/csv', canView, withDoc(async (activeDoc, req, res) => {
// Query DB for doc metadata to get the doc title. const options = await this._getDownloadOptions(req);
const {name: docTitle} = await this._dbManager.getDoc(req);
const options = this._getDownloadOptions(req, docTitle);
await downloadCSV(activeDoc, req, res, options); await downloadCSV(activeDoc, req, res, options);
})); }));
this._app.get('/api/docs/:docId/download/xlsx', canView, withDoc(async (activeDoc, req, res) => { this._app.get('/api/docs/:docId/download/xlsx', canView, withDoc(async (activeDoc, req, res) => {
// Query DB for doc metadata to get the doc title (to use as the filename). const options: DownloadOptions = (!_.isEmpty(req.query) && !_.isEqual(Object.keys(req.query), ["title"]))
const {name: docTitle} = await this._dbManager.getDoc(req); ? await this._getDownloadOptions(req)
const options: DownloadOptions = !_.isEmpty(req.query) ? this._getDownloadOptions(req, docTitle) : { : {
filename: docTitle, filename: await this._getDownloadFilename(req),
tableId: '', tableId: '',
viewSectionId: undefined, viewSectionId: undefined,
filters: [], filters: [],
@ -1734,11 +1736,23 @@ export class DocWorkerApi {
return docAuth.docId!; return docAuth.docId!;
} }
private _getDownloadOptions(req: Request, name: string): DownloadOptions { private async _getDownloadFilename(req: Request, tableId?: string, optDoc?: Document): Promise<string> {
let filename = optStringParam(req.query.title, 'title');
if (!filename) {
// Query DB for doc metadata to get the doc data.
const doc = optDoc || await this._dbManager.getDoc(req);
const docTitle = doc.name;
const suffix = tableId ? (tableId === docTitle ? '' : `-${tableId}`) : '';
filename = docTitle + suffix || 'document';
}
return filename;
}
private async _getDownloadOptions(req: Request, doc?: Document): Promise<DownloadOptions> {
const params = parseExportParameters(req); const params = parseExportParameters(req);
return { return {
...params, ...params,
filename: name + (params.tableId === name ? '' : '-' + params.tableId), filename: await this._getDownloadFilename(req, params.tableId, doc),
}; };
} }

View File

@ -68,14 +68,10 @@ export class DocWorker {
} }
public async downloadDoc(req: express.Request, res: express.Response, public async downloadDoc(req: express.Request, res: express.Response,
storageManager: IDocStorageManager): Promise<void> { storageManager: IDocStorageManager, filename: string): Promise<void> {
const mreq = req as RequestWithLogin; const mreq = req as RequestWithLogin;
const docId = getDocId(mreq); const docId = getDocId(mreq);
// Query DB for doc metadata to get the doc title.
const doc = await this._dbManager.getDoc(req);
const docTitle = doc.name;
// Get a copy of document for downloading. // Get a copy of document for downloading.
const tmpPath = await storageManager.getCopy(docId); const tmpPath = await storageManager.getCopy(docId);
if (isAffirmative(req.query.template)) { if (isAffirmative(req.query.template)) {
@ -90,7 +86,7 @@ export class DocWorker {
return res.type('application/x-sqlite3') return res.type('application/x-sqlite3')
.download( .download(
tmpPath, tmpPath,
(optStringParam(req.query.title, 'title') || docTitle || 'document') + ".grist", filename + ".grist",
async (err: any) => { async (err: any) => {
if (err) { if (err) {
if (err.message && /Request aborted/.test(err.message)) { if (err.message && /Request aborted/.test(err.message)) {

View File

@ -547,7 +547,9 @@
"You do not have write access to this site": "You do not have write access to this site", "You do not have write access to this site": "You do not have write access to this site",
"Download full document and history": "Download full document and history", "Download full document and history": "Download full document and history",
"Remove all data but keep the structure to use as a template": "Remove all data but keep the structure to use as a template", "Remove all data but keep the structure to use as a template": "Remove all data but keep the structure to use as a template",
"Remove document history (can significantly reduce file size)": "Remove document history (can significantly reduce file size)" "Remove document history (can significantly reduce file size)": "Remove document history (can significantly reduce file size)",
"Download": "Download",
"Download document": "Download document"
}, },
"NotifyUI": { "NotifyUI": {
"Ask for help": "Ask for help", "Ask for help": "Ask for help",