mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Refactor forms implementation
Summary: WIP Test Plan: Existing tests. Reviewers: jarek Reviewed By: jarek Subscribers: jarek Differential Revision: https://phab.getgrist.com/D4196
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import {FormLayoutNode} from 'app/client/components/FormRenderer';
|
||||
import {buildEditor} from 'app/client/components/Forms/Editor';
|
||||
import {FieldModel} from 'app/client/components/Forms/Field';
|
||||
import {buildMenu} from 'app/client/components/Forms/Menu';
|
||||
@@ -6,7 +7,6 @@ import * as style from 'app/client/components/Forms/styles';
|
||||
import {makeTestId} from 'app/client/lib/domUtils';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import * as menus from 'app/client/ui2018/menus';
|
||||
import {Box} from 'app/common/Forms';
|
||||
import {inlineStyle, not} from 'app/common/gutil';
|
||||
import {bundleChanges, Computed, dom, IDomArgs, MultiHolder, Observable, styled} from 'grainjs';
|
||||
|
||||
@@ -28,7 +28,7 @@ export class ColumnsModel extends BoxModel {
|
||||
}
|
||||
|
||||
// Dropping a box on this component (Columns) directly will add it as a new column.
|
||||
public accept(dropped: Box): BoxModel {
|
||||
public accept(dropped: FormLayoutNode): BoxModel {
|
||||
if (!this.parent) { throw new Error('No parent'); }
|
||||
|
||||
// We need to remove it from the parent, so find it first.
|
||||
@@ -206,7 +206,7 @@ export class PlaceholderModel extends BoxModel {
|
||||
...args,
|
||||
);
|
||||
|
||||
function insertBox(childBox: Box) {
|
||||
function insertBox(childBox: FormLayoutNode) {
|
||||
// Make sure we have at least as many columns as the index we are inserting at.
|
||||
if (!box.parent) { throw new Error('No parent'); }
|
||||
return box.parent.replace(box, childBox);
|
||||
@@ -218,15 +218,15 @@ export class PlaceholderModel extends BoxModel {
|
||||
}
|
||||
}
|
||||
|
||||
export function Paragraph(text: string, alignment?: 'left'|'right'|'center'): Box {
|
||||
export function Paragraph(text: string, alignment?: 'left'|'right'|'center'): FormLayoutNode {
|
||||
return {type: 'Paragraph', text, alignment};
|
||||
}
|
||||
|
||||
export function Placeholder(): Box {
|
||||
export function Placeholder(): FormLayoutNode {
|
||||
return {type: 'Placeholder'};
|
||||
}
|
||||
|
||||
export function Columns(): Box {
|
||||
export function Columns(): FormLayoutNode {
|
||||
return {type: 'Columns', children: [Placeholder(), Placeholder()]};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {CHOOSE_TEXT, FormLayoutNode} from 'app/client/components/FormRenderer';
|
||||
import {buildEditor} from 'app/client/components/Forms/Editor';
|
||||
import {FormView} from 'app/client/components/Forms/FormView';
|
||||
import {BoxModel, ignoreClick} from 'app/client/components/Forms/Model';
|
||||
@@ -7,7 +8,6 @@ import {refRecord} from 'app/client/models/DocModel';
|
||||
import {autoGrow} from 'app/client/ui/forms';
|
||||
import {squareCheckbox} from 'app/client/ui2018/checkbox';
|
||||
import {colors} from 'app/client/ui2018/cssVars';
|
||||
import {Box, CHOOSE_TEXT} from 'app/common/Forms';
|
||||
import {Constructor, not} from 'app/common/gutil';
|
||||
import {
|
||||
BindableValue,
|
||||
@@ -78,7 +78,7 @@ export class FieldModel extends BoxModel {
|
||||
return instance;
|
||||
});
|
||||
|
||||
constructor(box: Box, parent: BoxModel | null, view: FormView) {
|
||||
constructor(box: FormLayoutNode, parent: BoxModel | null, view: FormView) {
|
||||
super(box, parent, view);
|
||||
|
||||
this.required = Computed.create(this, (use) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import BaseView from 'app/client/components/BaseView';
|
||||
import * as commands from 'app/client/components/commands';
|
||||
import {Cursor} from 'app/client/components/Cursor';
|
||||
import {FormLayoutNode, FormLayoutNodeType} from 'app/client/components/FormRenderer';
|
||||
import * as components from 'app/client/components/Forms/elements';
|
||||
import {NewBox} from 'app/client/components/Forms/Menu';
|
||||
import {BoxModel, LayoutModel, parseBox, Place} from 'app/client/components/Forms/Model';
|
||||
@@ -16,13 +17,13 @@ import DataTableModel from 'app/client/models/DataTableModel';
|
||||
import {ViewFieldRec, ViewSectionRec} from 'app/client/models/DocModel';
|
||||
import {ShareRec} from 'app/client/models/entities/ShareRec';
|
||||
import {InsertColOptions} from 'app/client/models/entities/ViewSectionRec';
|
||||
import {urlState} from 'app/client/models/gristUrlState';
|
||||
import {docUrl, urlState} from 'app/client/models/gristUrlState';
|
||||
import {SortedRowSet} from 'app/client/models/rowset';
|
||||
import {showTransientTooltip} from 'app/client/ui/tooltips';
|
||||
import {cssButton} from 'app/client/ui2018/buttons';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {confirmModal} from 'app/client/ui2018/modals';
|
||||
import {Box, BoxType, INITIAL_FIELDS_COUNT} from "app/common/Forms";
|
||||
import {INITIAL_FIELDS_COUNT} from 'app/common/Forms';
|
||||
import {Events as BackboneEvents} from 'backbone';
|
||||
import {Computed, dom, Holder, IDomArgs, MultiHolder, Observable} from 'grainjs';
|
||||
import defaults from 'lodash/defaults';
|
||||
@@ -47,7 +48,7 @@ export class FormView extends Disposable {
|
||||
protected menuHolder: Holder<any>;
|
||||
protected bundle: (clb: () => Promise<void>) => Promise<void>;
|
||||
|
||||
private _autoLayout: Computed<Box>;
|
||||
private _autoLayout: Computed<FormLayoutNode>;
|
||||
private _root: BoxModel;
|
||||
private _savedLayout: any;
|
||||
private _saving: boolean = false;
|
||||
@@ -290,14 +291,14 @@ export class FormView extends Disposable {
|
||||
// Sanity check that type is correct.
|
||||
if (!colIds.every(c => typeof c === 'string')) { throw new Error('Invalid column id'); }
|
||||
this._root.save(async () => {
|
||||
const boxes: Box[] = [];
|
||||
const boxes: FormLayoutNode[] = [];
|
||||
for (const colId of colIds) {
|
||||
const fieldRef = await this.viewSection.showColumn(colId);
|
||||
const field = this.viewSection.viewFields().all().find(f => f.getRowId() === fieldRef);
|
||||
if (!field) { continue; }
|
||||
const box = {
|
||||
leaf: fieldRef,
|
||||
type: 'Field' as BoxType,
|
||||
type: 'Field' as FormLayoutNodeType,
|
||||
};
|
||||
boxes.push(box);
|
||||
}
|
||||
@@ -333,8 +334,7 @@ export class FormView extends Disposable {
|
||||
const doc = use(this.gristDoc.docPageModel.currentDoc);
|
||||
if (!doc) { return ''; }
|
||||
const url = urlState().makeUrl({
|
||||
api: true,
|
||||
doc: doc.id,
|
||||
...docUrl(doc),
|
||||
form: {
|
||||
vsId: use(this.viewSection.id),
|
||||
},
|
||||
@@ -723,11 +723,11 @@ export class FormView extends Disposable {
|
||||
* Generates a form template based on the fields in the view section.
|
||||
*/
|
||||
private _formTemplate(fields: ViewFieldRec[]) {
|
||||
const boxes: Box[] = fields.map(f => {
|
||||
const boxes: FormLayoutNode[] = fields.map(f => {
|
||||
return {
|
||||
type: 'Field',
|
||||
leaf: f.id()
|
||||
} as Box;
|
||||
} as FormLayoutNode;
|
||||
});
|
||||
const section = {
|
||||
type: 'Section',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {allCommands} from 'app/client/components/commands';
|
||||
import {FormLayoutNodeType} from 'app/client/components/FormRenderer';
|
||||
import * as components from 'app/client/components/Forms/elements';
|
||||
import {FormView} from 'app/client/components/Forms/FormView';
|
||||
import {BoxModel, Place} from 'app/client/components/Forms/Model';
|
||||
@@ -7,14 +8,13 @@ import {FocusLayer} from 'app/client/lib/FocusLayer';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {getColumnTypes as getNewColumnTypes} from 'app/client/ui/GridViewMenus';
|
||||
import * as menus from 'app/client/ui2018/menus';
|
||||
import {BoxType} from 'app/common/Forms';
|
||||
import {Computed, dom, IDomArgs, MultiHolder} from 'grainjs';
|
||||
|
||||
const t = makeT('FormView');
|
||||
const testId = makeTestId('test-forms-menu-');
|
||||
|
||||
// New box to add, either a new column of type, an existing column (by column id), or a structure.
|
||||
export type NewBox = {add: string} | {show: string} | {structure: BoxType};
|
||||
export type NewBox = {add: string} | {show: string} | {structure: FormLayoutNodeType};
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
@@ -77,7 +77,7 @@ export function buildMenu(props: Props, ...args: IDomArgs<HTMLElement>): IDomArg
|
||||
box?.view.selectedBox.set(box);
|
||||
|
||||
// Same for structure.
|
||||
const struct = (structure: BoxType) => ({structure});
|
||||
const struct = (structure: FormLayoutNodeType) => ({structure});
|
||||
|
||||
// Actions:
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {FormLayoutNode, FormLayoutNodeType} from 'app/client/components/FormRenderer';
|
||||
import * as elements from 'app/client/components/Forms/elements';
|
||||
import {FormView} from 'app/client/components/Forms/FormView';
|
||||
import {Box, BoxType} from 'app/common/Forms';
|
||||
import {bundleChanges, Computed, Disposable, dom, IDomArgs, MutableObsArray, obsArray, Observable} from 'grainjs';
|
||||
import {v4 as uuidv4} from 'uuid';
|
||||
|
||||
@@ -9,7 +9,7 @@ type Callback = () => Promise<void>;
|
||||
/**
|
||||
* A place where to insert a box.
|
||||
*/
|
||||
export type Place = (box: Box) => BoxModel;
|
||||
export type Place = (box: FormLayoutNode) => BoxModel;
|
||||
|
||||
/**
|
||||
* View model constructed from a box JSON structure.
|
||||
@@ -19,7 +19,7 @@ export abstract class BoxModel extends Disposable {
|
||||
/**
|
||||
* A factory method that creates a new BoxModel from a Box JSON by picking the right class based on the type.
|
||||
*/
|
||||
public static new(box: Box, parent: BoxModel | null, view: FormView | null = null): BoxModel {
|
||||
public static new(box: FormLayoutNode, parent: BoxModel | null, view: FormView | null = null): BoxModel {
|
||||
const subClassName = `${box.type.split(':')[0]}Model`;
|
||||
const factories = elements as any;
|
||||
const factory = factories[subClassName];
|
||||
@@ -42,7 +42,7 @@ export abstract class BoxModel extends Disposable {
|
||||
* Type of the box. As the type is bounded to the class that is used to render the box, it is possible
|
||||
* to change the type of the box just by changing this value. The box is then replaced in the parent.
|
||||
*/
|
||||
public type: BoxType;
|
||||
public type: FormLayoutNodeType;
|
||||
/**
|
||||
* List of children boxes.
|
||||
*/
|
||||
@@ -65,7 +65,7 @@ export abstract class BoxModel extends Disposable {
|
||||
/**
|
||||
* Don't use it directly, use the BoxModel.new factory method instead.
|
||||
*/
|
||||
constructor(box: Box, public parent: BoxModel | null, public view: FormView) {
|
||||
constructor(box: FormLayoutNode, public parent: BoxModel | null, public view: FormView) {
|
||||
super();
|
||||
|
||||
this.selected = Computed.create(this, (use) => use(view.selectedBox) === this && use(view.viewSection.hasFocus));
|
||||
@@ -149,7 +149,7 @@ export abstract class BoxModel extends Disposable {
|
||||
* - child: it will add it as a child.
|
||||
* - swap: swaps with the box
|
||||
*/
|
||||
public willAccept(box?: Box|BoxModel|null): 'sibling' | 'child' | 'swap' | null {
|
||||
public willAccept(box?: FormLayoutNode|BoxModel|null): 'sibling' | 'child' | 'swap' | null {
|
||||
// If myself and the dropped element share the same parent, and the parent is a column
|
||||
// element, just swap us.
|
||||
if (this.parent && box instanceof BoxModel && this.parent === box?.parent && box.parent?.type === 'Columns') {
|
||||
@@ -166,7 +166,7 @@ export abstract class BoxModel extends Disposable {
|
||||
* Accepts box from clipboard and inserts it before this box or if this is a container box, then
|
||||
* as a first child. Default implementation is to insert before self.
|
||||
*/
|
||||
public accept(dropped: Box, hint: 'above'|'below' = 'above') {
|
||||
public accept(dropped: FormLayoutNode, hint: 'above'|'below' = 'above') {
|
||||
// Get the box that was dropped.
|
||||
if (!dropped) { return null; }
|
||||
if (dropped.id === this.id) {
|
||||
@@ -200,7 +200,7 @@ export abstract class BoxModel extends Disposable {
|
||||
/**
|
||||
* Replaces children at index.
|
||||
*/
|
||||
public replaceAtIndex(box: Box, index: number) {
|
||||
public replaceAtIndex(box: FormLayoutNode, index: number) {
|
||||
const newOne = BoxModel.new(box, this);
|
||||
this.children.splice(index, 1, newOne);
|
||||
return newOne;
|
||||
@@ -216,13 +216,13 @@ export abstract class BoxModel extends Disposable {
|
||||
this.replace(box2, box1JSON);
|
||||
}
|
||||
|
||||
public append(box: Box) {
|
||||
public append(box: FormLayoutNode) {
|
||||
const newOne = BoxModel.new(box, this);
|
||||
this.children.push(newOne);
|
||||
return newOne;
|
||||
}
|
||||
|
||||
public insert(box: Box, index: number) {
|
||||
public insert(box: FormLayoutNode, index: number) {
|
||||
const newOne = BoxModel.new(box, this);
|
||||
this.children.splice(index, 0, newOne);
|
||||
return newOne;
|
||||
@@ -232,7 +232,7 @@ export abstract class BoxModel extends Disposable {
|
||||
/**
|
||||
* Replaces existing box with a new one, whenever it is found.
|
||||
*/
|
||||
public replace(existing: BoxModel, newOne: Box|BoxModel) {
|
||||
public replace(existing: BoxModel, newOne: FormLayoutNode|BoxModel) {
|
||||
const index = this.children.get().indexOf(existing);
|
||||
if (index < 0) { throw new Error('Cannot replace box that is not in parent'); }
|
||||
const model = newOne instanceof BoxModel ? newOne : BoxModel.new(newOne, this);
|
||||
@@ -246,20 +246,20 @@ export abstract class BoxModel extends Disposable {
|
||||
* Creates a place to insert a box before this box.
|
||||
*/
|
||||
public placeBeforeFirstChild() {
|
||||
return (box: Box) => this.insert(box, 0);
|
||||
return (box: FormLayoutNode) => this.insert(box, 0);
|
||||
}
|
||||
|
||||
// Some other places.
|
||||
public placeAfterListChild() {
|
||||
return (box: Box) => this.insert(box, this.children.get().length);
|
||||
return (box: FormLayoutNode) => this.insert(box, this.children.get().length);
|
||||
}
|
||||
|
||||
public placeAt(index: number) {
|
||||
return (box: Box) => this.insert(box, index);
|
||||
return (box: FormLayoutNode) => this.insert(box, index);
|
||||
}
|
||||
|
||||
public placeAfterChild(child: BoxModel) {
|
||||
return (box: Box) => this.insert(box, this.children.get().indexOf(child) + 1);
|
||||
return (box: FormLayoutNode) => this.insert(box, this.children.get().indexOf(child) + 1);
|
||||
}
|
||||
|
||||
public placeAfterMe() {
|
||||
@@ -319,7 +319,7 @@ export abstract class BoxModel extends Disposable {
|
||||
* The core responsibility of this method is to update this box and all children based on the box JSON.
|
||||
* This is counterpart of the FloatingRowModel, that enables this instance to point to a different box.
|
||||
*/
|
||||
public update(boxDef: Box) {
|
||||
public update(boxDef: FormLayoutNode) {
|
||||
// If we have a type and the type is changed, then we need to replace the box.
|
||||
if (this.type && boxDef.type !== this.type) {
|
||||
if (!this.parent) { throw new Error('Cannot replace detached box'); }
|
||||
@@ -329,7 +329,7 @@ export abstract class BoxModel extends Disposable {
|
||||
|
||||
// Update all properties of self.
|
||||
for (const someKey in boxDef) {
|
||||
const key = someKey as keyof Box;
|
||||
const key = someKey as keyof FormLayoutNode;
|
||||
// Skip some keys.
|
||||
if (key === 'id' || key === 'type' || key === 'children') { continue; }
|
||||
// Skip any inherited properties.
|
||||
@@ -365,7 +365,7 @@ export abstract class BoxModel extends Disposable {
|
||||
/**
|
||||
* Serialize this box to JSON.
|
||||
*/
|
||||
public toJSON(): Box {
|
||||
public toJSON(): FormLayoutNode {
|
||||
return {
|
||||
id: this.id,
|
||||
type: this.type,
|
||||
@@ -388,7 +388,7 @@ export abstract class BoxModel extends Disposable {
|
||||
|
||||
export class LayoutModel extends BoxModel {
|
||||
constructor(
|
||||
box: Box,
|
||||
box: FormLayoutNode,
|
||||
public parent: BoxModel | null,
|
||||
public _save: (clb?: Callback) => Promise<void>,
|
||||
public view: FormView
|
||||
@@ -420,7 +420,7 @@ export function unwrap<T>(val: T | Computed<T>): T {
|
||||
return val instanceof Computed ? val.get() : val;
|
||||
}
|
||||
|
||||
export function parseBox(text: string): Box|null {
|
||||
export function parseBox(text: string): FormLayoutNode|null {
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
return json && typeof json === 'object' && json.type ? json : null;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import * as style from './styles';
|
||||
import {FormLayoutNode} from 'app/client/components/FormRenderer';
|
||||
import {buildEditor} from 'app/client/components/Forms/Editor';
|
||||
import {FieldModel} from 'app/client/components/Forms/Field';
|
||||
import {buildMenu} from 'app/client/components/Forms/Menu';
|
||||
import {BoxModel} from 'app/client/components/Forms/Model';
|
||||
import * as style from 'app/client/components/Forms/styles';
|
||||
import {makeTestId} from 'app/client/lib/domUtils';
|
||||
import {Box} from 'app/common/Forms';
|
||||
import {dom, styled} from 'grainjs';
|
||||
|
||||
const testId = makeTestId('test-forms-');
|
||||
@@ -51,7 +51,7 @@ export class SectionModel extends BoxModel {
|
||||
* Accepts box from clipboard and inserts it before this box or if this is a container box, then
|
||||
* as a first child. Default implementation is to insert before self.
|
||||
*/
|
||||
public override accept(dropped: Box) {
|
||||
public override accept(dropped: FormLayoutNode) {
|
||||
// Get the box that was dropped.
|
||||
if (!dropped) { return null; }
|
||||
if (dropped.id === this.id) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {FormLayoutNode, FormLayoutNodeType} from 'app/client/components/FormRenderer';
|
||||
import {Columns, Paragraph, Placeholder} from 'app/client/components/Forms/Columns';
|
||||
import {Box, BoxType} from 'app/common/Forms';
|
||||
/**
|
||||
* Add any other element you whish to use in the form here.
|
||||
* FormView will look for any exported BoxModel derived class in format `type` + `Model`, and use It
|
||||
@@ -12,7 +12,7 @@ export * from './Columns';
|
||||
export * from './Submit';
|
||||
export * from './Label';
|
||||
|
||||
export function defaultElement(type: BoxType): Box {
|
||||
export function defaultElement(type: FormLayoutNodeType): FormLayoutNode {
|
||||
switch(type) {
|
||||
case 'Columns': return Columns();
|
||||
case 'Placeholder': return Placeholder();
|
||||
|
||||
Reference in New Issue
Block a user