(core) Allow duplicating tables from Raw Data page

Summary:
Adds a "Duplicate Table" menu option to the tables listed on
the Raw Data page. Clicking it opens a dialog that allows you to
make a copy of the table (with or without its data).

Test Plan: Python, server, and browser tests.

Reviewers: jarek, paulfitz

Reviewed By: jarek, paulfitz

Subscribers: jarek

Differential Revision: https://phab.getgrist.com/D3619
This commit is contained in:
George Gevoian
2022-09-28 16:01:46 -07:00
parent 0eb1fec3d7
commit cd64237dad
7 changed files with 459 additions and 27 deletions

View File

@@ -3,6 +3,7 @@ import {copyToClipboard} from 'app/client/lib/copyToClipboard';
import {setTestState} from 'app/client/lib/testState';
import {TableRec} from 'app/client/models/DocModel';
import {docListHeader, docMenuTrigger} from 'app/client/ui/DocMenuCss';
import {duplicateTable, DuplicateTableResponse} from 'app/client/ui/DuplicateTable';
import {showTransientTooltip} from 'app/client/ui/tooltips';
import {buildTableName} from 'app/client/ui/WidgetTitle';
import * as css from 'app/client/ui2018/cssVars';
@@ -121,6 +122,16 @@ export class DataTables extends Disposable {
private _menuItems(table: TableRec) {
const {isReadonly, docModel} = this._gristDoc;
return [
menuItem(
() => this._duplicateTable(table),
'Duplicate Table',
testId('menu-duplicate-table'),
dom.cls('disabled', use =>
use(isReadonly) ||
use(table.isHidden) ||
use(table.summarySourceTable) !== 0
),
),
menuItem(
() => this._removeTable(table),
'Remove',
@@ -134,6 +145,13 @@ export class DataTables extends Disposable {
];
}
private _duplicateTable(t: TableRec) {
duplicateTable(this._gristDoc, t.tableId(), {
onSuccess: ({raw_section_id}: DuplicateTableResponse) =>
this._gristDoc.viewModel.activeSectionId(raw_section_id),
});
}
private _removeTable(t: TableRec) {
const {docModel} = this._gristDoc;
function doRemove() {

View File

@@ -0,0 +1,122 @@
import {GristDoc} from 'app/client/components/GristDoc';
import {cssInput} from 'app/client/ui/cssInput';
import {cssField} from 'app/client/ui/MakeCopyMenu';
import {labeledSquareCheckbox} from 'app/client/ui2018/checkbox';
import {colors} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {cssLink} from 'app/client/ui2018/links';
import {saveModal} from 'app/client/ui2018/modals';
import {commonUrls} from 'app/common/gristUrls';
import {Computed, Disposable, dom, input, makeTestId, Observable, styled} from 'grainjs';
const testId = makeTestId('test-duplicate-table-');
/**
* Response returned by a DuplicateTable user action.
*/
export interface DuplicateTableResponse {
/** Row id of the new table. */
id: number;
/** Table id of the new table. */
table_id: string;
/** Row id of the new raw view section. */
raw_section_id: number;
}
export interface DuplicateTableOptions {
onSuccess?(response: DuplicateTableResponse): void;
}
/**
* Shows a modal with options for duplicating the table `tableId`.
*/
export function duplicateTable(
gristDoc: GristDoc,
tableId: string,
{onSuccess}: DuplicateTableOptions = {}
) {
saveModal((_ctl, owner) => {
const duplicateTableModal = DuplicateTableModal.create(owner, gristDoc, tableId);
return {
title: 'Duplicate Table',
body: duplicateTableModal.buildDom(),
saveFunc: async () => {
const response = await duplicateTableModal.save();
onSuccess?.(response);
},
saveDisabled: duplicateTableModal.saveDisabled,
width: 'normal',
};
});
}
class DuplicateTableModal extends Disposable {
private _newTableName = Observable.create<string>(this, '');
private _includeData = Observable.create<boolean>(this, false);
private _saveDisabled = Computed.create(this, this._newTableName, (_use, name) => !name.trim());
constructor(private _gristDoc: GristDoc, private _tableId: string) {
super();
}
public get saveDisabled() { return this._saveDisabled; }
public save() {
return this._duplicateTable();
}
public buildDom() {
return [
cssField(
input(
this._newTableName,
{onInput: true},
{placeholder: 'Name for new table'},
(elem) => { setTimeout(() => { elem.focus(); }, 20); },
dom.on('focus', (_ev, elem) => { elem.select(); }),
dom.cls(cssInput.className),
testId('name'),
),
),
cssWarning(
cssWarningIcon('Warning'),
dom('div',
"Instead of duplicating tables, it's usually better to segment data using linked views. ",
cssLink({href: commonUrls.helpLinkingWidgets, target: '_blank'}, 'Read More.')
),
),
cssField(
cssCheckbox(
this._includeData,
'Copy all data in addition to the table structure.',
testId('copy-all-data'),
),
),
dom.maybe(this._includeData, () => cssWarning(
cssWarningIcon('Warning'),
dom('div', 'Only the document default access rules will apply to the copy.'),
testId('acl-warning'),
)),
];
}
private _duplicateTable() {
const {docData} = this._gristDoc;
const [newTableName, includeData] = [this._newTableName.get(), this._includeData.get()];
return docData.sendAction(['DuplicateTable', this._tableId, newTableName, includeData]);
}
}
const cssCheckbox = styled(labeledSquareCheckbox, `
margin-top: 8px;
`);
const cssWarning = styled('div', `
display: flex;
column-gap: 8px;
`);
const cssWarningIcon = styled(icon, `
--icon-color: ${colors.orange};
flex-shrink: 0;
`);