mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
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:
parent
011cf9da0d
commit
9ce8ed3f25
@ -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,
|
||||||
),
|
),
|
||||||
|
@ -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();
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
@ -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),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)) {
|
||||||
|
@ -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",
|
||||||
|
Loading…
Reference in New Issue
Block a user