Feat: add env var to add context (workspace and organization) to exported filename

pull/872/head
CamilleLegeron 4 months ago
parent ed137c1fa1
commit 88ec8af80d

@ -255,6 +255,7 @@ GRIST_HOST | hostname to use when listening on a port.
GRIST_HTTPS_PROXY | if set, use this proxy for webhook payload delivery.
GRIST_ID_PREFIX | for subdomains of form o-*, expect or produce o-${GRIST_ID_PREFIX}*.
GRIST_IGNORE_SESSION | if set, Grist will not use a session for authentication.
GRIST_INCLUDE_CONTEXT_TO_DOWNLOAD_FILENAMES | if set to true the organization name and the worspace name will be added to downloaded filenames, by default is set to false.
GRIST_INST_DIR | path to Grist instance configuration files, for Grist server.
GRIST_LIST_PUBLIC_SITES | if set to true, sites shared with the public will be listed for anonymous users. Defaults to false.
GRIST_MANAGED_WORKERS | if set, Grist can assume that if a url targeted at a doc worker returns a 404, that worker is gone

@ -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.')
),
dom('div',
cssLink({href: commonUrls.helpTryingOutChanges, target: '_blank'}, 'Learn more.'),
cssLink({href: commonUrls.helpTryingOutChanges, target: '_blank'}, t('Learn more.')),
),
...args,
),

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

@ -276,7 +276,7 @@ function menuExports(doc: Document, pageModel: DocPageModel) {
menuItem(() => gristDoc.app.comm.showItemInFolder(doc.name),
t("Show in folder"), testId('tb-share-option')) :
menuItem(() => downloadDocModal(doc, pageModel),
menuIcon('Download'), t("Download..."), testId('tb-share-option'))
menuIcon('Download'), t("Download"), "...", testId('tb-share-option'))
),
menuItemLink(hooks.maybeModifyLinkAttrs({ href: gristDoc.getCsvLink(), target: '_blank', download: ''}),
menuIcon('Download'), t("Export CSV"), testId('tb-share-option')),

@ -646,6 +646,9 @@ export class DocWorkerApi {
// full document.
const dryRun = isAffirmative(req.query.dryrun || req.query.dryRun);
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
// to load. So, if the user owns the document, we unconditionally let them
// download.
@ -655,13 +658,13 @@ export class DocWorkerApi {
// 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
// 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) {
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
// we try again after having created an ActiveDoc for the document.
await this._getActiveDoc(req);
return this._docWorker.downloadDoc(req, res, this._docManager.storageManager);
return this._docWorker.downloadDoc(req, res, this._docManager.storageManager, filename);
} else {
throw e;
}
@ -674,7 +677,7 @@ export class DocWorkerApi {
throw new ApiError('not authorized to download this document', 403);
}
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 +1225,7 @@ export class DocWorkerApi {
this._app.get('/api/docs/:docId/download/table-schema', canView, withDoc(async (activeDoc, req, res) => {
const doc = await this._dbManager.getDoc(req);
const options = this._getDownloadOptions(req, doc.name);
const options = await this._getDownloadOptions(req);
const tableSchema = await collectTableSchemaInFrictionlessFormat(activeDoc, req, options);
const apiPath = await this._grist.getResourceUrl(doc, 'api');
const query = new URLSearchParams(req.query as {[key: string]: string});
@ -1241,18 +1244,14 @@ export class DocWorkerApi {
}));
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 {name: docTitle} = await this._dbManager.getDoc(req);
const options = this._getDownloadOptions(req, docTitle);
const options = await this._getDownloadOptions(req);
await downloadCSV(activeDoc, req, res, options);
}));
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 {name: docTitle} = await this._dbManager.getDoc(req);
const options: DownloadOptions = !_.isEmpty(req.query) ? this._getDownloadOptions(req, docTitle) : {
filename: docTitle,
const options: DownloadOptions = !_.isEmpty(req.query) ? await this._getDownloadOptions(req) : {
filename: await this._getDownloadFilename(req),
tableId: '',
viewSectionId: undefined,
filters: [],
@ -1734,11 +1733,24 @@ export class DocWorkerApi {
return docAuth.docId!;
}
private _getDownloadOptions(req: Request, name: string): DownloadOptions {
private async _getDownloadFilename(req: Request, tableId?: string): Promise<string> {
const addContext = process.env.GRIST_INCLUDE_CONTEXT_TO_DOWNLOAD_FILENAMES || false;
// Query DB for doc metadata to get the doc data.
const doc = await this._dbManager.getDoc(req);
const docTitle = doc.name;
const prefix = addContext ? `${doc.workspace?.org?.name}_${doc.workspace?.name}_` : '';
const sufix = tableId ? (tableId === docTitle ? '' : `_${tableId}`) : '';
const filename = prefix + (optStringParam(req.query.title, 'title') || docTitle || 'document') + sufix;
return filename;
}
private async _getDownloadOptions(req: Request): Promise<DownloadOptions> {
const params = parseExportParameters(req);
return {
...params,
filename: name + (params.tableId === name ? '' : '-' + params.tableId),
filename: await this._getDownloadFilename(req, params.tableId),
};
}

@ -68,14 +68,10 @@ export class DocWorker {
}
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 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.
const tmpPath = await storageManager.getCopy(docId);
if (isAffirmative(req.query.template)) {
@ -90,7 +86,7 @@ export class DocWorker {
return res.type('application/x-sqlite3')
.download(
tmpPath,
(optStringParam(req.query.title, 'title') || docTitle || 'document') + ".grist",
filename + ".grist",
async (err: any) => {
if (err) {
if (err.message && /Request aborted/.test(err.message)) {

@ -547,7 +547,9 @@
"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",
"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": {
"Ask for help": "Ask for help",

@ -655,7 +655,7 @@
"Export XLSX": "Exporter en XLSX",
"Send to Google Drive": "Envoyer vers Google Drive",
"Share": "Partager",
"Download...": "Télécharger..."
"Download...": "Télécharger"
},
"SiteSwitcher": {
"Switch Sites": "Changer despace",

Loading…
Cancel
Save