mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
180 lines
7.5 KiB
TypeScript
180 lines
7.5 KiB
TypeScript
|
import { GristDoc } from 'app/client/components/GristDoc';
|
||
|
import { ViewFieldRec, ViewSectionRec } from 'app/client/models/DocModel';
|
||
|
import { cssField, cssInput, cssLabel} from 'app/client/ui/MakeCopyMenu';
|
||
|
import { IPageWidget, toPageWidget } from 'app/client/ui/PageWidgetPicker';
|
||
|
import { confirmModal } from 'app/client/ui2018/modals';
|
||
|
import { BulkColValues, RowRecord, UserAction } from 'app/common/DocActions';
|
||
|
import { arrayRepeat } from 'app/common/gutil';
|
||
|
import { schema } from 'app/common/schema';
|
||
|
import { dom } from 'grainjs';
|
||
|
import cloneDeepWith = require('lodash/cloneDeepWith');
|
||
|
import flatten = require('lodash/flatten');
|
||
|
import forEach = require('lodash/forEach');
|
||
|
import zip = require('lodash/zip');
|
||
|
import zipObject = require('lodash/zipObject');
|
||
|
|
||
|
// Duplicate page with pageId. Starts by prompting user for a new name.
|
||
|
export async function duplicatePage(gristDoc: GristDoc, pageId: number) {
|
||
|
const pagesTable = gristDoc.docModel.pages;
|
||
|
const pageName = pagesTable.rowModels[pageId].view.peek().name.peek();
|
||
|
let inputEl: HTMLInputElement;
|
||
|
setTimeout(() => {inputEl.focus(); inputEl.select(); }, 100);
|
||
|
|
||
|
confirmModal('Duplicate page', 'Save', () => makeDuplicate(gristDoc, pageId, inputEl.value), (
|
||
|
dom('div', [
|
||
|
"Enter name for the new page. ",
|
||
|
"Note that this does not copy data, ",
|
||
|
"but creates another view of the same data. ",
|
||
|
cssField(
|
||
|
cssLabel("Name"),
|
||
|
inputEl = cssInput({value: pageName + ' (copy)'}),
|
||
|
)
|
||
|
])
|
||
|
));
|
||
|
}
|
||
|
|
||
|
async function makeDuplicate(gristDoc: GristDoc, pageId: number, pageName: string = '') {
|
||
|
const sourceView = gristDoc.docModel.pages.rowModels[pageId].view.peek();
|
||
|
pageName = pageName || `${sourceView.name.peek()} (copy)`;
|
||
|
const viewSections = sourceView.viewSections.peek().peek();
|
||
|
let viewRef = 0;
|
||
|
await gristDoc.docData.bundleActions(
|
||
|
`Duplicate page ${pageName}`,
|
||
|
async () => {
|
||
|
// create new view and new sections
|
||
|
const results = await createNewViewSections(gristDoc.docData, viewSections);
|
||
|
viewRef = results[0].viewRef;
|
||
|
|
||
|
// give it a better name
|
||
|
await gristDoc.docModel.views.rowModels[viewRef].name.saveOnly(pageName);
|
||
|
|
||
|
// create a map from source to target section ids
|
||
|
const viewSectionIdMap = zipObject(
|
||
|
viewSections.map(vs => vs.getRowId()),
|
||
|
results.map(res => res.sectionRef)
|
||
|
) as {[id: number]: number};
|
||
|
|
||
|
// update layout spec
|
||
|
const viewLayoutSpec = patchLayoutSpec(sourceView.layoutSpecObj.peek(), viewSectionIdMap);
|
||
|
await gristDoc.docData.sendAction(
|
||
|
['UpdateRecord', '_grist_Views', viewRef, { layoutSpec: JSON.stringify(viewLayoutSpec)}]
|
||
|
);
|
||
|
|
||
|
// update the view fields
|
||
|
const destViewSections = viewSections.map((vs) => (
|
||
|
gristDoc.docModel.viewSections.rowModels[viewSectionIdMap[vs.getRowId()]]
|
||
|
));
|
||
|
const newViewFieldIds = await updateViewFields(gristDoc, destViewSections, viewSections);
|
||
|
|
||
|
// create map for mapping from a src field's id to its corresponding dest field's id
|
||
|
const viewFieldsIdMap = zipObject(
|
||
|
flatten(viewSections.map((vs) => vs.viewFields.peek().peek().map((field) => field.getRowId()))),
|
||
|
flatten(newViewFieldIds)) as {[id: number]: number};
|
||
|
|
||
|
// update the view sections
|
||
|
await updateViewSections(gristDoc, destViewSections, viewSections, viewFieldsIdMap, viewSectionIdMap);
|
||
|
});
|
||
|
|
||
|
// Give copy focus
|
||
|
await gristDoc.openDocPage(viewRef);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Update all of destViewSections with srcViewSections, use fieldsMap to patch the section layout
|
||
|
* (for detail/cardlist sections), use viewSectionMap to patch the sections ids for linking.
|
||
|
*/
|
||
|
async function updateViewSections(gristDoc: GristDoc, destViewSections: ViewSectionRec[],
|
||
|
srcViewSections: ViewSectionRec[], fieldsMap: {[id: number]: number},
|
||
|
viewSectionMap: {[id: number]: number}) {
|
||
|
|
||
|
// collect all the records for the src view sections
|
||
|
const records: RowRecord[] = [];
|
||
|
for (const srcViewSection of srcViewSections) {
|
||
|
const viewSectionLayoutSpec = patchLayoutSpec(srcViewSection.layoutSpecObj.peek(), fieldsMap);
|
||
|
const record = gristDoc.docData.getTable('_grist_Views_section')!.getRecord(srcViewSection.getRowId())!;
|
||
|
records.push({
|
||
|
...record,
|
||
|
layoutSpec: JSON.stringify(viewSectionLayoutSpec),
|
||
|
linkSrcSectionRef: viewSectionMap[srcViewSection.linkSrcSectionRef.peek()],
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// transpose data
|
||
|
const sectionsInfo = {} as BulkColValues;
|
||
|
forEach(records[0], (val, key) => sectionsInfo[key] = records.map(rec => rec[key]));
|
||
|
|
||
|
// ditch column ids and parentId
|
||
|
delete sectionsInfo.id;
|
||
|
delete sectionsInfo.parentId;
|
||
|
|
||
|
// send action
|
||
|
const rowIds = destViewSections.map((vs) => vs.getRowId());
|
||
|
await gristDoc.docData.sendAction(['BulkUpdateRecord', '_grist_Views_section', rowIds, sectionsInfo]);
|
||
|
}
|
||
|
|
||
|
async function updateViewFields(gristDoc: GristDoc, destViewSections: ViewSectionRec[],
|
||
|
srcViewSections: ViewSectionRec[]) {
|
||
|
const actions: UserAction[] = [];
|
||
|
const docData = gristDoc.docData;
|
||
|
|
||
|
// First, remove all existing fields. Needed because `CreateViewSections` adds some by default.
|
||
|
const toRemove = flatten(destViewSections.map((vs) => vs.viewFields.peek().peek().map((field) => field.getRowId())));
|
||
|
actions.push(['BulkRemoveRecord', '_grist_Views_section_field', toRemove]);
|
||
|
|
||
|
// collect all the fields to add
|
||
|
const fieldsToAdd: RowRecord[] = [];
|
||
|
for (const [destViewSection, srcViewSection] of zip(destViewSections, srcViewSections)) {
|
||
|
const srcViewFields: ViewFieldRec[] = srcViewSection!.viewFields.peek().peek();
|
||
|
const parentId = destViewSection!.getRowId();
|
||
|
for (const field of srcViewFields) {
|
||
|
const record = docData.getTable('_grist_Views_section_field')!.getRecord(field.getRowId())!;
|
||
|
fieldsToAdd.push({...record, parentId});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// transpose data
|
||
|
const fieldsInfo = {} as BulkColValues;
|
||
|
forEach(schema._grist_Views_section_field, (val, key) => fieldsInfo[key] = fieldsToAdd.map(rec => rec[key]));
|
||
|
const rowIds = arrayRepeat(fieldsInfo.parentId.length, null);
|
||
|
actions.push(['BulkAddRecord', '_grist_Views_section_field', rowIds, fieldsInfo]);
|
||
|
|
||
|
const results = await gristDoc.docData.sendActions(actions);
|
||
|
return results[1];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create a new view containing all of the viewSections. Note that it doesn't copy view fields, for
|
||
|
* which you can use `updateViewFields`.
|
||
|
*/
|
||
|
async function createNewViewSections(docData: GristDoc['docData'], viewSections: ViewSectionRec[]) {
|
||
|
const [first, ...rest] = viewSections.map(toPageWidget);
|
||
|
|
||
|
// Passing a viewId of 0 will create a new view.
|
||
|
const firstResult = await docData.sendAction(newViewSectionAction(first, 0));
|
||
|
|
||
|
const otherResult = await docData.sendActions(
|
||
|
// other view section are added to the newly created view
|
||
|
rest.map((widget) => newViewSectionAction(widget, firstResult.viewRef))
|
||
|
);
|
||
|
return [firstResult, ...otherResult];
|
||
|
}
|
||
|
|
||
|
// Helper to create an action that add widget to the view with viewId.
|
||
|
function newViewSectionAction(widget: IPageWidget, viewId: number) {
|
||
|
return ['CreateViewSection', widget.table, viewId, widget.type, widget.summarize ? widget.columns : null];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Replaces each `leaf` id in layoutSpec by its corresponding id in mapIds. Leave unchanged if id is
|
||
|
* missing from mapIds.
|
||
|
*/
|
||
|
export function patchLayoutSpec(layoutSpec: any, mapIds: {[id: number]: number}) {
|
||
|
return cloneDeepWith(layoutSpec, (val) => {
|
||
|
if (typeof val === 'object') {
|
||
|
if (mapIds[val.leaf]) {
|
||
|
return {...val, leaf: mapIds[val.leaf]};
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
}
|