mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Add dialog with options to allow downloading without history or data
Summary:
{F74398}
Refactored the 'radio checkboxes' in the modal for deleting a page and reused them here.
The option to download as a template already existed in the server code but wasn't being exercised by the frontend. Also added an option to remove just the history, which is the main motivation for this diff.
Test Plan: Expanded the existing nbrowser test.
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3999
This commit is contained in:
@@ -5,15 +5,24 @@
|
||||
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {AppModel, reportError} from 'app/client/models/AppModel';
|
||||
import {DocPageModel} from "app/client/models/DocPageModel";
|
||||
import {getLoginOrSignupUrl, urlState} from 'app/client/models/gristUrlState';
|
||||
import {getWorkspaceInfo, ownerName, workspaceName} from 'app/client/models/WorkspaceInfo';
|
||||
import {cssInput} from 'app/client/ui/cssInput';
|
||||
import {bigBasicButton, bigPrimaryButtonLink} from 'app/client/ui2018/buttons';
|
||||
import {labeledSquareCheckbox} from 'app/client/ui2018/checkbox';
|
||||
import {cssRadioCheckboxOptions, labeledSquareCheckbox, radioCheckboxOption} from 'app/client/ui2018/checkbox';
|
||||
import {testId, theme, vars} from 'app/client/ui2018/cssVars';
|
||||
import {loadingSpinner} from 'app/client/ui2018/loaders';
|
||||
import {select} from 'app/client/ui2018/menus';
|
||||
import {confirmModal, cssModalBody, cssModalButtons, cssModalWidth, modal, saveModal} from 'app/client/ui2018/modals';
|
||||
import {
|
||||
confirmModal,
|
||||
cssModalBody,
|
||||
cssModalButtons,
|
||||
cssModalTitle,
|
||||
cssModalWidth,
|
||||
modal,
|
||||
saveModal
|
||||
} from 'app/client/ui2018/modals';
|
||||
import {FullUser} from 'app/common/LoginSessionAPI';
|
||||
import * as roles from 'app/common/roles';
|
||||
import {Document, isTemplatesOrg, Organization, Workspace} from 'app/common/UserAPI';
|
||||
@@ -278,6 +287,43 @@ class SaveCopyModal extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
type DownloadOption = 'full' | 'nohistory' | 'template';
|
||||
|
||||
export function downloadDocModal(doc: Document, pageModel: DocPageModel) {
|
||||
return modal((ctl, owner) => {
|
||||
const selected = Observable.create<DownloadOption>(owner, 'full');
|
||||
|
||||
return [
|
||||
cssModalTitle(`Download document`),
|
||||
cssRadioCheckboxOptions(
|
||||
radioCheckboxOption(selected, 'full', t("Download full document and history")),
|
||||
radioCheckboxOption(selected, 'nohistory', t("Remove document history (can significantly reduce file size)")),
|
||||
radioCheckboxOption(selected, 'template', t("Remove all data but keep the structure to use as a template")),
|
||||
),
|
||||
cssModalButtons(
|
||||
dom.domComputed(use =>
|
||||
bigPrimaryButtonLink(`Download`, {
|
||||
href: pageModel.appModel.api.getDocAPI(doc.id).getDownloadUrl({
|
||||
template: use(selected) === "template",
|
||||
removeHistory: use(selected) === "nohistory" || use(selected) === "template",
|
||||
}),
|
||||
target: '_blank',
|
||||
download: ''
|
||||
},
|
||||
dom.on('click', () => {
|
||||
ctl.close();
|
||||
}),
|
||||
testId('download-button-link'),
|
||||
),
|
||||
),
|
||||
bigBasicButton('Cancel', dom.on('click', () => {
|
||||
ctl.close();
|
||||
}))
|
||||
)
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
export const cssField = styled('div', `
|
||||
margin: 16px 0;
|
||||
display: flex;
|
||||
|
||||
@@ -8,13 +8,12 @@ import MetaTableModel from 'app/client/models/MetaTableModel';
|
||||
import {find as findInTree, fromTableData, TreeItemRecord, TreeRecord,
|
||||
TreeTableData} from 'app/client/models/TreeModel';
|
||||
import {TreeViewComponent} from 'app/client/ui/TreeViewComponent';
|
||||
import {labeledCircleCheckbox} from 'app/client/ui2018/checkbox';
|
||||
import {theme} from 'app/client/ui2018/cssVars';
|
||||
import {cssRadioCheckboxOptions, radioCheckboxOption} from 'app/client/ui2018/checkbox';
|
||||
import {cssLink} from 'app/client/ui2018/links';
|
||||
import {ISaveModalOptions, saveModal} from 'app/client/ui2018/modals';
|
||||
import {buildCensoredPage, buildPageDom, PageActions} from 'app/client/ui2018/pages';
|
||||
import {mod} from 'app/common/gutil';
|
||||
import {Computed, Disposable, dom, DomContents, fromKo, makeTestId, observable, Observable, styled} from 'grainjs';
|
||||
import {Computed, Disposable, dom, fromKo, makeTestId, observable, Observable, styled} from 'grainjs';
|
||||
|
||||
const t = makeT('Pages');
|
||||
|
||||
@@ -141,9 +140,9 @@ function buildPrompt(tableNames: string[], onSave: (option: RemoveOption) => Pro
|
||||
body: dom('div',
|
||||
testId('popup'),
|
||||
buildWarning(tableNames),
|
||||
cssOptions(
|
||||
buildOption(selected, 'data', t("Delete data and this page.")),
|
||||
buildOption(selected, 'page',
|
||||
cssRadioCheckboxOptions(
|
||||
radioCheckboxOption(selected, 'data', t("Delete data and this page.")),
|
||||
radioCheckboxOption(selected, 'page',
|
||||
[ // TODO i18n
|
||||
`Keep data and delete page. `,
|
||||
`Table will remain available in `,
|
||||
@@ -161,16 +160,6 @@ function buildPrompt(tableNames: string[], onSave: (option: RemoveOption) => Pro
|
||||
});
|
||||
}
|
||||
|
||||
function buildOption(value: Observable<RemoveOption>, id: RemoveOption, content: DomContents) {
|
||||
const selected = Computed.create(null, use => use(value) === id)
|
||||
.onWrite(val => val ? value.set(id) : void 0);
|
||||
return dom.update(
|
||||
labeledCircleCheckbox(selected, content, dom.autoDispose(selected)),
|
||||
testId(`option-${id}`),
|
||||
cssBlockCheckbox.cls(''),
|
||||
cssBlockCheckbox.cls('-block', selected),
|
||||
);
|
||||
}
|
||||
|
||||
function buildWarning(tables: string[]) {
|
||||
return cssWarning(
|
||||
@@ -178,37 +167,6 @@ function buildWarning(tables: string[]) {
|
||||
);
|
||||
}
|
||||
|
||||
const cssOptions = styled('div', `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
`);
|
||||
|
||||
// We need to reset top and left of ::before element, as it is wrongly set
|
||||
// on the inline checkbox.
|
||||
// To simulate radio button behavior, we will block user input after option is selected, because
|
||||
// checkbox doesn't support two-way binding.
|
||||
const cssBlockCheckbox = styled('div', `
|
||||
display: flex;
|
||||
padding: 10px 8px;
|
||||
border: 1px solid ${theme.modalBorder};
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
& input::before, & input::after {
|
||||
top: unset;
|
||||
left: unset;
|
||||
}
|
||||
&:hover {
|
||||
border-color: ${theme.accentBorder};
|
||||
}
|
||||
&-block {
|
||||
pointer-events: none;
|
||||
}
|
||||
&-block a {
|
||||
pointer-events: all;
|
||||
}
|
||||
`);
|
||||
|
||||
const cssWarning = styled('div', `
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -3,7 +3,7 @@ import {AppModel, reportError} from 'app/client/models/AppModel';
|
||||
import {DocInfo, DocPageModel} from 'app/client/models/DocPageModel';
|
||||
import {docUrl, urlState} from 'app/client/models/gristUrlState';
|
||||
import {GristTooltips} from 'app/client/ui/GristTooltips';
|
||||
import {makeCopy, replaceTrunkWithFork} from 'app/client/ui/MakeCopyMenu';
|
||||
import {downloadDocModal, makeCopy, replaceTrunkWithFork} from 'app/client/ui/MakeCopyMenu';
|
||||
import {sendToDrive} from 'app/client/ui/sendToDrive';
|
||||
import {hoverTooltip, withInfoTooltip} from 'app/client/ui/tooltips';
|
||||
import {cssHoverCircle, cssTopBarBtn} from 'app/client/ui/TopBarCss';
|
||||
@@ -252,11 +252,8 @@ function menuExports(doc: Document, pageModel: DocPageModel) {
|
||||
(isElectron ?
|
||||
menuItem(() => gristDoc.app.comm.showItemInFolder(doc.name),
|
||||
t("Show in folder"), testId('tb-share-option')) :
|
||||
menuItemLink({
|
||||
href: pageModel.appModel.api.getDocAPI(doc.id).getDownloadUrl(),
|
||||
target: '_blank', download: ''
|
||||
},
|
||||
menuIcon('Download'), t("Download"), testId('tb-share-option'))
|
||||
menuItem(() => downloadDocModal(doc, pageModel),
|
||||
menuIcon('Download'), t("Download..."), testId('tb-share-option'))
|
||||
),
|
||||
menuItemLink({ href: gristDoc.getCsvLink(), target: '_blank', download: ''},
|
||||
menuIcon('Download'), t("Export CSV"), testId('tb-share-option')),
|
||||
|
||||
@@ -15,9 +15,8 @@
|
||||
* labeledSquareCheckbox(observable(false), 'Include other values', dom.prop('disabled', true)),
|
||||
*/
|
||||
|
||||
import { theme } from 'app/client/ui2018/cssVars';
|
||||
import { Computed, dom, DomArg, styled } from 'grainjs';
|
||||
import { Observable } from 'grainjs';
|
||||
import {testId, theme} from 'app/client/ui2018/cssVars';
|
||||
import {Computed, dom, DomArg, DomContents, Observable, styled} from 'grainjs';
|
||||
|
||||
export const cssLabel = styled('label', `
|
||||
position: relative;
|
||||
@@ -176,6 +175,48 @@ export function labeledTriStateSquareCheckbox(obs: Observable<TriState>, label:
|
||||
return triStateCheckbox(obs, cssCheckboxSquare, label, ...domArgs);
|
||||
}
|
||||
|
||||
export function radioCheckboxOption<T>(selectedObservable: Observable<T>, optionId: T, content: DomContents) {
|
||||
const selected = Computed.create(null, use => use(selectedObservable) === optionId)
|
||||
.onWrite(val => val ? selectedObservable.set(optionId) : void 0);
|
||||
return dom.update(
|
||||
labeledCircleCheckbox(selected, content, dom.autoDispose(selected)),
|
||||
testId(`option-${optionId}`),
|
||||
cssBlockCheckbox.cls(''),
|
||||
cssBlockCheckbox.cls('-block', selected),
|
||||
);
|
||||
}
|
||||
|
||||
export const cssRadioCheckboxOptions = styled('div', `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
`);
|
||||
|
||||
// We need to reset top and left of ::before element, as it is wrongly set
|
||||
// on the inline checkbox.
|
||||
// To simulate radio button behavior, we will block user input after option is selected, because
|
||||
// checkbox doesn't support two-way binding.
|
||||
const cssBlockCheckbox = styled('div', `
|
||||
display: flex;
|
||||
padding: 10px 8px;
|
||||
border: 1px solid ${theme.modalBorder};
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
& input::before, & input::after {
|
||||
top: unset;
|
||||
left: unset;
|
||||
}
|
||||
&:hover {
|
||||
border-color: ${theme.accentBorder};
|
||||
}
|
||||
&-block {
|
||||
pointer-events: none;
|
||||
}
|
||||
&-block a {
|
||||
pointer-events: all;
|
||||
}
|
||||
`);
|
||||
|
||||
const cssInlineRelative = styled('div', `
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
|
||||
Reference in New Issue
Block a user