mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) move client code to core
Summary: This moves all client code to core, and makes minimal fix-ups to get grist and grist-core to compile correctly. The client works in core, but I'm leaving clean-up around the build and bundles to follow-up. Test Plan: existing tests pass; server-dev bundle looks sane Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2627
This commit is contained in:
179
app/client/components/duplicatePage.ts
Normal file
179
app/client/components/duplicatePage.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
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]};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user