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:
14
app/client/models/entities/ACLMembershipRec.ts
Normal file
14
app/client/models/entities/ACLMembershipRec.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import {ACLPrincipalRec, DocModel, IRowModel, refRecord} from 'app/client/models/DocModel';
|
||||
import * as ko from 'knockout';
|
||||
|
||||
// Table for containment relationships between Principals, e.g. user contains multiple
|
||||
// instances, group contains multiple users, and groups may contain other groups.
|
||||
export interface ACLMembershipRec extends IRowModel<"_grist_ACLMemberships"> {
|
||||
parentRec: ko.Computed<ACLPrincipalRec>;
|
||||
childRec: ko.Computed<ACLPrincipalRec>;
|
||||
}
|
||||
|
||||
export function createACLMembershipRec(this: ACLMembershipRec, docModel: DocModel): void {
|
||||
this.parentRec = refRecord(docModel.aclPrincipals, this.parent);
|
||||
this.childRec = refRecord(docModel.aclPrincipals, this.child);
|
||||
}
|
||||
29
app/client/models/entities/ACLPrincipalRec.ts
Normal file
29
app/client/models/entities/ACLPrincipalRec.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import {KoArray} from 'app/client/lib/koArray';
|
||||
import {ACLMembershipRec, DocModel, IRowModel, recordSet} from 'app/client/models/DocModel';
|
||||
import {KoSaveableObservable} from 'app/client/models/modelUtil';
|
||||
import * as ko from 'knockout';
|
||||
|
||||
// A principals used by ACL rules, including users, groups, and instances.
|
||||
export interface ACLPrincipalRec extends IRowModel<"_grist_ACLPrincipals"> {
|
||||
// Declare a more specific type for 'type' than what's set automatically from schema.ts.
|
||||
type: KoSaveableObservable<'user'|'instance'|'group'>;
|
||||
|
||||
// KoArray of ACLMembership row models which contain this principal as a child.
|
||||
parentMemberships: ko.Computed<KoArray<ACLMembershipRec>>;
|
||||
|
||||
// Gives an array of ACLPrincipal parents to this row model.
|
||||
parents: ko.Computed<ACLPrincipalRec[]>;
|
||||
|
||||
// KoArray of ACLMembership row models which contain this principal as a parent.
|
||||
childMemberships: ko.Computed<KoArray<ACLMembershipRec>>;
|
||||
|
||||
// Gives an array of ACLPrincipal children of this row model.
|
||||
children: ko.Computed<ACLPrincipalRec[]>;
|
||||
}
|
||||
|
||||
export function createACLPrincipalRec(this: ACLPrincipalRec, docModel: DocModel): void {
|
||||
this.parentMemberships = recordSet(this, docModel.aclMemberships, 'child');
|
||||
this.childMemberships = recordSet(this, docModel.aclMemberships, 'parent');
|
||||
this.parents = ko.pureComputed(() => this.parentMemberships().all().map(m => m.parentRec()));
|
||||
this.children = ko.pureComputed(() => this.childMemberships().all().map(m => m.childRec()));
|
||||
}
|
||||
7
app/client/models/entities/ACLResourceRec.ts
Normal file
7
app/client/models/entities/ACLResourceRec.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import {DocModel, IRowModel} from 'app/client/models/DocModel';
|
||||
|
||||
export type ACLResourceRec = IRowModel<"_grist_ACLResources">;
|
||||
|
||||
export function createACLResourceRec(this: ACLResourceRec, docModel: DocModel): void {
|
||||
// no extra fields
|
||||
}
|
||||
92
app/client/models/entities/ColumnRec.ts
Normal file
92
app/client/models/entities/ColumnRec.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import {KoArray} from 'app/client/lib/koArray';
|
||||
import {DocModel, IRowModel, recordSet, refRecord, TableRec, ViewFieldRec} from 'app/client/models/DocModel';
|
||||
import {jsonObservable, ObjObservable} from 'app/client/models/modelUtil';
|
||||
import * as gristTypes from 'app/common/gristTypes';
|
||||
import {removePrefix} from 'app/common/gutil';
|
||||
import * as ko from 'knockout';
|
||||
|
||||
// Represents a column in a user-defined table.
|
||||
export interface ColumnRec extends IRowModel<"_grist_Tables_column"> {
|
||||
table: ko.Computed<TableRec>;
|
||||
widgetOptionsJson: ObjObservable<any>;
|
||||
viewFields: ko.Computed<KoArray<ViewFieldRec>>;
|
||||
summarySource: ko.Computed<ColumnRec>;
|
||||
|
||||
// Is an empty column (undecided if formula or data); denoted by an empty formula.
|
||||
isEmpty: ko.Computed<boolean>;
|
||||
|
||||
// Is a real formula column (not an empty column; i.e. contains a non-empty formula).
|
||||
isRealFormula: ko.Computed<boolean>;
|
||||
|
||||
// Used for transforming a column.
|
||||
// Reference to the original column for a transform column, or to itself for a non-transforming column.
|
||||
origColRef: ko.Observable<number>;
|
||||
origCol: ko.Computed<ColumnRec>;
|
||||
// Indicates whether a column is transforming. Manually set, but should be true in both the original
|
||||
// column being transformed and that column's transform column.
|
||||
isTransforming: ko.Observable<boolean>;
|
||||
|
||||
// Convenience observable to obtain and set the type with no suffix
|
||||
pureType: ko.Computed<string>;
|
||||
|
||||
// The column's display column
|
||||
_displayColModel: ko.Computed<ColumnRec>;
|
||||
|
||||
disableModify: ko.Computed<boolean>;
|
||||
disableEditData: ko.Computed<boolean>;
|
||||
|
||||
isHiddenCol: ko.Computed<boolean>;
|
||||
|
||||
// Returns the rowModel for the referenced table, or null, if is not a reference column.
|
||||
refTable: ko.Computed<TableRec|null>;
|
||||
|
||||
// Helper which adds/removes/updates column's displayCol to match the formula.
|
||||
saveDisplayFormula(formula: string): Promise<void>|undefined;
|
||||
}
|
||||
|
||||
export function createColumnRec(this: ColumnRec, docModel: DocModel): void {
|
||||
this.table = refRecord(docModel.tables, this.parentId);
|
||||
this.widgetOptionsJson = jsonObservable(this.widgetOptions);
|
||||
this.viewFields = recordSet(this, docModel.viewFields, 'colRef');
|
||||
this.summarySource = refRecord(docModel.columns, this.summarySourceCol);
|
||||
|
||||
// Is this an empty column (undecided if formula or data); denoted by an empty formula.
|
||||
this.isEmpty = ko.pureComputed(() => this.isFormula() && this.formula() === '');
|
||||
|
||||
// Is this a real formula column (not an empty column; i.e. contains a non-empty formula).
|
||||
this.isRealFormula = ko.pureComputed(() => this.isFormula() && this.formula() !== '');
|
||||
|
||||
// Used for transforming a column.
|
||||
// Reference to the original column for a transform column, or to itself for a non-transforming column.
|
||||
this.origColRef = ko.observable(this.getRowId());
|
||||
this.origCol = refRecord(docModel.columns, this.origColRef);
|
||||
// Indicates whether a column is transforming. Manually set, but should be true in both the original
|
||||
// column being transformed and that column's transform column.
|
||||
this.isTransforming = ko.observable(false);
|
||||
|
||||
// Convenience observable to obtain and set the type with no suffix
|
||||
this.pureType = ko.pureComputed(() => gristTypes.extractTypeFromColType(this.type()));
|
||||
|
||||
// The column's display column
|
||||
this._displayColModel = refRecord(docModel.columns, this.displayCol);
|
||||
|
||||
// Helper which adds/removes/updates this column's displayCol to match the formula.
|
||||
this.saveDisplayFormula = function(formula) {
|
||||
if (formula !== (this._displayColModel().formula() || '')) {
|
||||
return docModel.docData.sendAction(["SetDisplayFormula", this.table().tableId(),
|
||||
null, this.getRowId(), formula]);
|
||||
}
|
||||
};
|
||||
|
||||
this.disableModify = ko.pureComputed(() => Boolean(this.summarySourceCol()));
|
||||
this.disableEditData = ko.pureComputed(() => Boolean(this.summarySourceCol()));
|
||||
|
||||
this.isHiddenCol = ko.pureComputed(() => this.colId().startsWith('gristHelper_') ||
|
||||
this.colId() === 'manualSort');
|
||||
|
||||
// Returns the rowModel for the referenced table, or null, if this is not a reference column.
|
||||
this.refTable = ko.pureComputed(() => {
|
||||
const refTableId = removePrefix(this.type() || "", 'Ref:');
|
||||
return refTableId ? docModel.allTables.all().find(t => t.tableId() === refTableId) || null : null;
|
||||
});
|
||||
}
|
||||
19
app/client/models/entities/DocInfoRec.ts
Normal file
19
app/client/models/entities/DocInfoRec.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import {DocModel, IRowModel} from 'app/client/models/DocModel';
|
||||
import * as ko from 'knockout';
|
||||
|
||||
// The document-wide metadata. It's all contained in a single record with id=1.
|
||||
export interface DocInfoRec extends IRowModel<"_grist_DocInfo"> {
|
||||
defaultViewId: ko.Computed<number>;
|
||||
newDefaultViewId: ko.Computed<number>;
|
||||
}
|
||||
|
||||
export function createDocInfoRec(this: DocInfoRec, docModel: DocModel): void {
|
||||
this.defaultViewId = this.autoDispose(ko.pureComputed(() => {
|
||||
const tab = docModel.allTabs.at(0);
|
||||
return tab ? tab.viewRef() : 0;
|
||||
}));
|
||||
this.newDefaultViewId = this.autoDispose(ko.pureComputed(() => {
|
||||
const page = docModel.allDocPages.at(0);
|
||||
return page ? page.viewRef() : 0;
|
||||
}));
|
||||
}
|
||||
11
app/client/models/entities/PageRec.ts
Normal file
11
app/client/models/entities/PageRec.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import {DocModel, IRowModel, refRecord, ViewRec} from 'app/client/models/DocModel';
|
||||
import * as ko from 'knockout';
|
||||
|
||||
// Represents a page entry in the tree of pages.
|
||||
export interface PageRec extends IRowModel<"_grist_Pages"> {
|
||||
view: ko.Computed<ViewRec>;
|
||||
}
|
||||
|
||||
export function createPageRec(this: PageRec, docModel: DocModel): void {
|
||||
this.view = refRecord(docModel.views, this.viewRef);
|
||||
}
|
||||
8
app/client/models/entities/REPLRec.ts
Normal file
8
app/client/models/entities/REPLRec.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import {DocModel, IRowModel} from 'app/client/models/DocModel';
|
||||
|
||||
// Record of input code and output text and error info for REPL.
|
||||
export type REPLRec = IRowModel<"_grist_REPL_Hist">
|
||||
|
||||
export function createREPLRec(this: REPLRec, docModel: DocModel): void {
|
||||
// no extra fields
|
||||
}
|
||||
11
app/client/models/entities/TabBarRec.ts
Normal file
11
app/client/models/entities/TabBarRec.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import {DocModel, IRowModel, refRecord, ViewRec} from 'app/client/models/DocModel';
|
||||
import * as ko from 'knockout';
|
||||
|
||||
// Represents a page entry in the tree of pages.
|
||||
export interface TabBarRec extends IRowModel<"_grist_TabBar"> {
|
||||
view: ko.Computed<ViewRec>;
|
||||
}
|
||||
|
||||
export function createTabBarRec(this: TabBarRec, docModel: DocModel): void {
|
||||
this.view = refRecord(docModel.views, this.viewRef);
|
||||
}
|
||||
77
app/client/models/entities/TableRec.ts
Normal file
77
app/client/models/entities/TableRec.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import {KoArray} from 'app/client/lib/koArray';
|
||||
import {DocModel, IRowModel, recordSet, refRecord} from 'app/client/models/DocModel';
|
||||
import {ColumnRec, TableViewRec, ValidationRec, ViewRec} from 'app/client/models/DocModel';
|
||||
import {MANUALSORT} from 'app/common/gristTypes';
|
||||
import * as ko from 'knockout';
|
||||
import toUpper = require('lodash/toUpper');
|
||||
import * as randomcolor from 'randomcolor';
|
||||
|
||||
// Represents a user-defined table.
|
||||
export interface TableRec extends IRowModel<"_grist_Tables"> {
|
||||
columns: ko.Computed<KoArray<ColumnRec>>;
|
||||
validations: ko.Computed<KoArray<ValidationRec>>;
|
||||
|
||||
primaryView: ko.Computed<ViewRec>;
|
||||
tableViewItems: ko.Computed<KoArray<TableViewRec>>;
|
||||
summarySource: ko.Computed<TableRec>;
|
||||
|
||||
// A Set object of colRefs for all summarySourceCols of table.
|
||||
summarySourceColRefs: ko.Computed<Set<number>>;
|
||||
|
||||
// tableId for normal tables, or tableId of the source table for summary tables.
|
||||
primaryTableId: ko.Computed<string>;
|
||||
|
||||
// The list of grouped by columns.
|
||||
groupByColumns: ko.Computed<ColumnRec[]>;
|
||||
|
||||
// The user-friendly name of the table, which is the same as tableId for non-summary tables,
|
||||
// and is 'tableId[groupByCols...]' for summary tables.
|
||||
tableTitle: ko.Computed<string>;
|
||||
|
||||
tableColor: string;
|
||||
disableAddRemoveRows: ko.Computed<boolean>;
|
||||
supportsManualSort: ko.Computed<boolean>;
|
||||
}
|
||||
|
||||
export function createTableRec(this: TableRec, docModel: DocModel): void {
|
||||
this.columns = recordSet(this, docModel.columns, 'parentId', {sortBy: 'parentPos'});
|
||||
this.validations = recordSet(this, docModel.validations, 'tableRef');
|
||||
|
||||
this.primaryView = refRecord(docModel.views, this.primaryViewId);
|
||||
this.tableViewItems = recordSet(this, docModel.tableViews, 'tableRef', {sortBy: 'viewRef'});
|
||||
this.summarySource = refRecord(docModel.tables, this.summarySourceTable);
|
||||
|
||||
// A Set object of colRefs for all summarySourceCols of this table.
|
||||
this.summarySourceColRefs = this.autoDispose(ko.pureComputed(() => new Set(
|
||||
this.columns().all().map(c => c.summarySourceCol()).filter(colRef => colRef))));
|
||||
|
||||
// tableId for normal tables, or tableId of the source table for summary tables.
|
||||
this.primaryTableId = ko.pureComputed(() =>
|
||||
this.summarySourceTable() ? this.summarySource().tableId() : this.tableId());
|
||||
|
||||
this.groupByColumns = ko.pureComputed(() => this.columns().all().filter(c => c.summarySourceCol()));
|
||||
|
||||
const groupByDesc = ko.pureComputed(() => {
|
||||
const groupBy = this.groupByColumns();
|
||||
return groupBy.length ? 'by ' + groupBy.map(c => c.label()).join(", ") : "Totals";
|
||||
});
|
||||
|
||||
// The user-friendly name of the table, which is the same as tableId for non-summary tables,
|
||||
// and is 'tableId[groupByCols...]' for summary tables.
|
||||
this.tableTitle = ko.pureComputed(() => {
|
||||
if (this.summarySourceTable()) {
|
||||
return toUpper(this.summarySource().tableId()) + " [" + groupByDesc() + "]";
|
||||
}
|
||||
return toUpper(this.tableId());
|
||||
});
|
||||
|
||||
// TODO: We should save this value and let users change it.
|
||||
this.tableColor = randomcolor({
|
||||
luminosity: 'light',
|
||||
seed: typeof this.id() === 'number' ? 5 * this.id() : this.id()
|
||||
});
|
||||
|
||||
this.disableAddRemoveRows = ko.pureComputed(() => Boolean(this.summarySourceTable()));
|
||||
|
||||
this.supportsManualSort = ko.pureComputed(() => this.columns().all().some(c => c.colId() === MANUALSORT));
|
||||
}
|
||||
13
app/client/models/entities/TableViewRec.ts
Normal file
13
app/client/models/entities/TableViewRec.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import {DocModel, IRowModel, refRecord, TableRec, ViewRec} from 'app/client/models/DocModel';
|
||||
import * as ko from 'knockout';
|
||||
|
||||
// Used in old-style list of views grouped by table.
|
||||
export interface TableViewRec extends IRowModel<"_grist_TableViews"> {
|
||||
table: ko.Computed<TableRec>;
|
||||
view: ko.Computed<ViewRec>;
|
||||
}
|
||||
|
||||
export function createTableViewRec(this: TableViewRec, docModel: DocModel): void {
|
||||
this.table = refRecord(docModel.tables, this.tableRef);
|
||||
this.view = refRecord(docModel.views, this.viewRef);
|
||||
}
|
||||
8
app/client/models/entities/ValidationRec.ts
Normal file
8
app/client/models/entities/ValidationRec.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import {DocModel, IRowModel} from 'app/client/models/DocModel';
|
||||
|
||||
// Represents a validation rule.
|
||||
export type ValidationRec = IRowModel<"_grist_Validations">
|
||||
|
||||
export function createValidationRec(this: ValidationRec, docModel: DocModel): void {
|
||||
// no extra fields
|
||||
}
|
||||
204
app/client/models/entities/ViewFieldRec.ts
Normal file
204
app/client/models/entities/ViewFieldRec.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import {ColumnRec, DocModel, IRowModel, refRecord, ViewSectionRec} from 'app/client/models/DocModel';
|
||||
import * as modelUtil from 'app/client/models/modelUtil';
|
||||
import * as UserType from 'app/client/widgets/UserType';
|
||||
import {BaseFormatter, createFormatter} from 'app/common/ValueFormatter';
|
||||
import {Computed, fromKo} from 'grainjs';
|
||||
import * as ko from 'knockout';
|
||||
|
||||
// Represents a page entry in the tree of pages.
|
||||
export interface ViewFieldRec extends IRowModel<"_grist_Views_section_field"> {
|
||||
viewSection: ko.Computed<ViewSectionRec>;
|
||||
widthDef: modelUtil.KoSaveableObservable<number>;
|
||||
|
||||
widthPx: ko.Computed<string>;
|
||||
column: ko.Computed<ColumnRec>;
|
||||
origCol: ko.Computed<ColumnRec>;
|
||||
colId: ko.Computed<string>;
|
||||
label: ko.Computed<string>;
|
||||
|
||||
// displayLabel displays label by default but switches to the more helpful colId whenever a
|
||||
// formula field in the view is being edited.
|
||||
displayLabel: modelUtil.KoSaveableObservable<string>;
|
||||
|
||||
// The field knows when we are editing a formula, so that all rows can reflect that.
|
||||
editingFormula: ko.Computed<boolean>;
|
||||
|
||||
// CSS class to add to formula cells, incl. to show that we are editing field's formula.
|
||||
formulaCssClass: ko.Computed<string|null>;
|
||||
|
||||
// The fields's display column
|
||||
_displayColModel: ko.Computed<ColumnRec>;
|
||||
|
||||
// Whether field uses column's widgetOptions (true) or its own (false).
|
||||
// During transform, use the transform column's options (which should be initialized to match
|
||||
// field or column when the transform starts TODO).
|
||||
useColOptions: ko.Computed<boolean>;
|
||||
|
||||
// Helper that returns the RowModel for either field or its column, depending on
|
||||
// useColOptions. Field and Column have a few identical fields:
|
||||
// .widgetOptions() // JSON string of options
|
||||
// .saveDisplayFormula() // Method to save the display formula
|
||||
// .displayCol() // Reference to an optional associated display column.
|
||||
_fieldOrColumn: ko.Computed<ColumnRec|ViewFieldRec>;
|
||||
|
||||
// Display col ref to use for the field, defaulting to the plain column itself.
|
||||
displayColRef: ko.Computed<number>;
|
||||
|
||||
visibleColRef: modelUtil.KoSaveableObservable<number>;
|
||||
|
||||
// The display column to use for the field, or the column itself when no displayCol is set.
|
||||
displayColModel: ko.Computed<ColumnRec>;
|
||||
visibleColModel: ko.Computed<ColumnRec>;
|
||||
|
||||
// The widgetOptions to read and write: either the column's or the field's own.
|
||||
_widgetOptionsStr: modelUtil.KoSaveableObservable<string>;
|
||||
|
||||
// Observable for the object with the current options, either for the field or for the column,
|
||||
// which takes into account the default options for column's type.
|
||||
|
||||
widgetOptionsJson: modelUtil.SaveableObjObservable<any>;
|
||||
|
||||
// Observable for the parsed filter object saved to the field.
|
||||
activeFilter: modelUtil.CustomComputed<string>;
|
||||
|
||||
// Computed boolean that's true when there's a saved filter
|
||||
isFiltered: Computed<boolean>;
|
||||
|
||||
disableModify: ko.Computed<boolean>;
|
||||
disableEditData: ko.Computed<boolean>;
|
||||
|
||||
textColor: modelUtil.KoSaveableObservable<string>;
|
||||
fillColor: modelUtil.KoSaveableObservable<string>;
|
||||
|
||||
// Helper which adds/removes/updates field's displayCol to match the formula.
|
||||
saveDisplayFormula(formula: string): Promise<void>|undefined;
|
||||
|
||||
// Helper for Reference columns, which returns a formatter according to the visibleCol
|
||||
// associated with field. Subscribes to observables if used within a computed.
|
||||
createVisibleColFormatter(): BaseFormatter;
|
||||
}
|
||||
|
||||
export function createViewFieldRec(this: ViewFieldRec, docModel: DocModel): void {
|
||||
this.viewSection = refRecord(docModel.viewSections, this.parentId);
|
||||
this.widthDef = modelUtil.fieldWithDefault(this.width, () => this.viewSection().defaultWidth());
|
||||
|
||||
this.widthPx = ko.pureComputed(() => this.widthDef() + 'px');
|
||||
this.column = refRecord(docModel.columns, this.colRef);
|
||||
this.origCol = ko.pureComputed(() => this.column().origCol());
|
||||
this.colId = ko.pureComputed(() => this.column().colId());
|
||||
this.label = ko.pureComputed(() => this.column().label());
|
||||
|
||||
// displayLabel displays label by default but switches to the more helpful colId whenever a
|
||||
// formula field in the view is being edited.
|
||||
this.displayLabel = modelUtil.savingComputed({
|
||||
read: () => docModel.editingFormula() ? '$' + this.origCol().colId() : this.origCol().label(),
|
||||
write: (setter, val) => setter(this.column().label, val)
|
||||
});
|
||||
|
||||
// The field knows when we are editing a formula, so that all rows can reflect that.
|
||||
const _editingFormula = ko.observable(false);
|
||||
this.editingFormula = ko.pureComputed({
|
||||
read: () => _editingFormula(),
|
||||
write: val => {
|
||||
// Whenever any view field changes its editingFormula status, let the docModel know.
|
||||
docModel.editingFormula(val);
|
||||
_editingFormula(val);
|
||||
}
|
||||
});
|
||||
|
||||
// CSS class to add to formula cells, incl. to show that we are editing this field's formula.
|
||||
this.formulaCssClass = ko.pureComputed<string|null>(() => {
|
||||
const col = this.column();
|
||||
return this.column().isTransforming() ? "transform_field" :
|
||||
(this.editingFormula() ? "formula_field_edit" :
|
||||
(col.isFormula() && col.formula() !== "" ? "formula_field" : null));
|
||||
});
|
||||
|
||||
// The fields's display column
|
||||
this._displayColModel = refRecord(docModel.columns, this.displayCol);
|
||||
|
||||
// Helper which adds/removes/updates this field's displayCol to match the formula.
|
||||
this.saveDisplayFormula = function(formula) {
|
||||
if (formula !== (this._displayColModel().formula() || '')) {
|
||||
return docModel.docData.sendAction(["SetDisplayFormula", this.column().table().tableId(),
|
||||
this.getRowId(), null, formula]);
|
||||
}
|
||||
};
|
||||
|
||||
// Whether this field uses column's widgetOptions (true) or its own (false).
|
||||
// During transform, use the transform column's options (which should be initialized to match
|
||||
// field or column when the transform starts TODO).
|
||||
this.useColOptions = ko.pureComputed(() => !this.widgetOptions() || this.column().isTransforming());
|
||||
|
||||
// Helper that returns the RowModel for either this field or its column, depending on
|
||||
// useColOptions. Field and Column have a few identical fields:
|
||||
// .widgetOptions() // JSON string of options
|
||||
// .saveDisplayFormula() // Method to save the display formula
|
||||
// .displayCol() // Reference to an optional associated display column.
|
||||
this._fieldOrColumn = ko.pureComputed(() => this.useColOptions() ? this.column() : this);
|
||||
|
||||
// Display col ref to use for the field, defaulting to the plain column itself.
|
||||
this.displayColRef = ko.pureComputed(() => this._fieldOrColumn().displayCol() || this.colRef());
|
||||
|
||||
this.visibleColRef = modelUtil.addSaveInterface(ko.pureComputed({
|
||||
read: () => this._fieldOrColumn().visibleCol(),
|
||||
write: (colRef) => this._fieldOrColumn().visibleCol(colRef),
|
||||
}),
|
||||
colRef => docModel.docData.bundleActions(null, async () => {
|
||||
const col = docModel.columns.getRowModel(colRef);
|
||||
await Promise.all([
|
||||
this._fieldOrColumn().visibleCol.saveOnly(colRef),
|
||||
this._fieldOrColumn().saveDisplayFormula(colRef ? `$${this.colId()}.${col.colId()}` : '')
|
||||
]);
|
||||
})
|
||||
);
|
||||
|
||||
// The display column to use for the field, or the column itself when no displayCol is set.
|
||||
this.displayColModel = refRecord(docModel.columns, this.displayColRef);
|
||||
this.visibleColModel = refRecord(docModel.columns, this.visibleColRef);
|
||||
|
||||
// Helper for Reference columns, which returns a formatter according to the visibleCol
|
||||
// associated with this field. If no visible column available, return formatting for the field itself.
|
||||
// Subscribes to observables if used within a computed.
|
||||
// TODO: It would be better to replace this with a pureComputed whose value is a formatter.
|
||||
this.createVisibleColFormatter = function() {
|
||||
const vcol = this.visibleColModel();
|
||||
return (vcol.getRowId() !== 0) ?
|
||||
createFormatter(vcol.type(), vcol.widgetOptionsJson()) :
|
||||
createFormatter(this.column().type(), this.widgetOptionsJson());
|
||||
};
|
||||
|
||||
// The widgetOptions to read and write: either the column's or the field's own.
|
||||
this._widgetOptionsStr = modelUtil.savingComputed({
|
||||
read: () => this._fieldOrColumn().widgetOptions(),
|
||||
write: (setter, val) => setter(this._fieldOrColumn().widgetOptions, val)
|
||||
});
|
||||
|
||||
// Observable for the object with the current options, either for the field or for the column,
|
||||
// which takes into account the default options for this column's type.
|
||||
|
||||
this.widgetOptionsJson = modelUtil.jsonObservable(this._widgetOptionsStr,
|
||||
(opts: any) => UserType.mergeOptions(opts || {}, this.column().pureType()));
|
||||
|
||||
// Observable for the active filter that's initialized from the value saved to the server.
|
||||
this.activeFilter = modelUtil.customComputed({
|
||||
read: () => { const f = this.filter(); return f === 'null' ? '' : f; }, // To handle old empty filters
|
||||
save: (val) => this.filter.saveOnly(val),
|
||||
});
|
||||
|
||||
this.isFiltered = Computed.create(this, fromKo(this.activeFilter), (_use, f) => f !== '');
|
||||
|
||||
this.disableModify = ko.pureComputed(() => this.column().disableModify());
|
||||
this.disableEditData = ko.pureComputed(() => this.column().disableEditData());
|
||||
|
||||
this.textColor = modelUtil.fieldWithDefault(
|
||||
this.widgetOptionsJson.prop('textColor') as modelUtil.KoSaveableObservable<string>, "#000000");
|
||||
|
||||
const fillColorProp = this.widgetOptionsJson.prop('fillColor') as modelUtil.KoSaveableObservable<string>;
|
||||
// Store empty string in place of the default white color, so that we can keep it transparent in
|
||||
// GridView, to avoid interfering with zebra stripes.
|
||||
this.fillColor = modelUtil.savingComputed({
|
||||
read: () => fillColorProp(),
|
||||
write: (setter, val) => setter(fillColorProp, val === '#ffffff' ? '' : val),
|
||||
});
|
||||
}
|
||||
53
app/client/models/entities/ViewRec.ts
Normal file
53
app/client/models/entities/ViewRec.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import {KoArray} from 'app/client/lib/koArray';
|
||||
import * as koUtil from 'app/client/lib/koUtil';
|
||||
import {DocModel, IRowModel, recordSet, refRecord} from 'app/client/models/DocModel';
|
||||
import {TabBarRec, TableViewRec, ViewSectionRec} from 'app/client/models/DocModel';
|
||||
import * as modelUtil from 'app/client/models/modelUtil';
|
||||
import * as ko from 'knockout';
|
||||
|
||||
// Represents a view (now also referred to as a "page") containing one or more view sections.
|
||||
export interface ViewRec extends IRowModel<"_grist_Views"> {
|
||||
viewSections: ko.Computed<KoArray<ViewSectionRec>>;
|
||||
tableViewItems: ko.Computed<KoArray<TableViewRec>>;
|
||||
tabBarItem: ko.Computed<KoArray<TabBarRec>>;
|
||||
|
||||
layoutSpecObj: modelUtil.ObjObservable<any>;
|
||||
|
||||
// An observable for the ref of the section last selected by the user.
|
||||
activeSectionId: ko.Computed<number>;
|
||||
|
||||
activeSection: ko.Computed<ViewSectionRec>;
|
||||
|
||||
// If the active section is removed, set the next active section to be the default.
|
||||
_isActiveSectionGone: ko.Computed<boolean>;
|
||||
|
||||
isLinking: ko.Observable<boolean>;
|
||||
}
|
||||
|
||||
export function createViewRec(this: ViewRec, docModel: DocModel): void {
|
||||
this.viewSections = recordSet(this, docModel.viewSections, 'parentId');
|
||||
this.tableViewItems = recordSet(this, docModel.tableViews, 'viewRef', {sortBy: 'tableRef'});
|
||||
this.tabBarItem = recordSet(this, docModel.tabBar, 'viewRef');
|
||||
|
||||
this.layoutSpecObj = modelUtil.jsonObservable(this.layoutSpec);
|
||||
|
||||
// An observable for the ref of the section last selected by the user.
|
||||
this.activeSectionId = koUtil.observableWithDefault(ko.observable(), () => {
|
||||
// The default function which is used when the conditional case is true.
|
||||
// Read may occur for recently disposed sections, must check condition first.
|
||||
return !this.isDisposed() &&
|
||||
this.viewSections().all().length > 0 ? this.viewSections().at(0)!.getRowId() : 0;
|
||||
});
|
||||
|
||||
this.activeSection = refRecord(docModel.viewSections, this.activeSectionId);
|
||||
|
||||
// If the active section is removed, set the next active section to be the default.
|
||||
this._isActiveSectionGone = this.autoDispose(ko.computed(() => this.activeSection()._isDeleted()));
|
||||
this.autoDispose(this._isActiveSectionGone.subscribe(gone => {
|
||||
if (gone) {
|
||||
this.activeSectionId(0);
|
||||
}
|
||||
}));
|
||||
|
||||
this.isLinking = ko.observable(false);
|
||||
}
|
||||
275
app/client/models/entities/ViewSectionRec.ts
Normal file
275
app/client/models/entities/ViewSectionRec.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
import * as BaseView from 'app/client/components/BaseView';
|
||||
import {CursorPos} from 'app/client/components/Cursor';
|
||||
import {KoArray} from 'app/client/lib/koArray';
|
||||
import {ColumnRec, TableRec, ViewFieldRec, ViewRec} from 'app/client/models/DocModel';
|
||||
import {DocModel, IRowModel, recordSet, refRecord} from 'app/client/models/DocModel';
|
||||
import * as modelUtil from 'app/client/models/modelUtil';
|
||||
import {RowId} from 'app/client/models/rowset';
|
||||
import {getWidgetTypes} from 'app/client/ui/widgetTypes';
|
||||
import {Computed} from 'grainjs';
|
||||
import * as ko from 'knockout';
|
||||
import defaults = require('lodash/defaults');
|
||||
|
||||
// Represents a section of user views, now also known as a "page widget" (e.g. a view may contain
|
||||
// a grid section and a chart section).
|
||||
export interface ViewSectionRec extends IRowModel<"_grist_Views_section"> {
|
||||
viewFields: ko.Computed<KoArray<ViewFieldRec>>;
|
||||
|
||||
optionsObj: modelUtil.SaveableObjObservable<any>;
|
||||
|
||||
customDef: CustomViewSectionDef;
|
||||
|
||||
themeDef: modelUtil.KoSaveableObservable<string>;
|
||||
chartTypeDef: modelUtil.KoSaveableObservable<string>;
|
||||
view: ko.Computed<ViewRec>;
|
||||
|
||||
table: ko.Computed<TableRec>;
|
||||
|
||||
tableTitle: ko.Computed<string>;
|
||||
titleDef: modelUtil.KoSaveableObservable<string>;
|
||||
|
||||
borderWidthPx: ko.Computed<string>;
|
||||
|
||||
layoutSpecObj: modelUtil.ObjObservable<any>;
|
||||
|
||||
// Helper metadata item which indicates whether any of the section's fields have unsaved
|
||||
// changes to their filters. (True indicates unsaved changes)
|
||||
filterSpecChanged: Computed<boolean>;
|
||||
|
||||
// Array of fields with an active filter
|
||||
filteredFields: Computed<ViewFieldRec[]>;
|
||||
|
||||
// Customizable version of the JSON-stringified sort spec. It may diverge from the saved one.
|
||||
activeSortJson: modelUtil.CustomComputed<string>;
|
||||
|
||||
// is an array (parsed from JSON) of colRefs (i.e. rowIds into the columns table), with a
|
||||
// twist: a rowId may be positive or negative, for ascending or descending respectively.
|
||||
activeSortSpec: modelUtil.ObjObservable<number[]>;
|
||||
|
||||
// Modified sort spec to take into account any active display columns.
|
||||
activeDisplaySortSpec: ko.Computed<number[]>;
|
||||
|
||||
// Evaluates to an array of column models, which are not referenced by anything in viewFields.
|
||||
hiddenColumns: ko.Computed<ColumnRec[]>;
|
||||
|
||||
hasFocus: ko.Computed<boolean>;
|
||||
|
||||
activeLinkSrcSectionRef: modelUtil.CustomComputed<number>;
|
||||
activeLinkSrcColRef: modelUtil.CustomComputed<number>;
|
||||
activeLinkTargetColRef: modelUtil.CustomComputed<number>;
|
||||
|
||||
// Whether current linking state is as saved. It may be different during editing.
|
||||
isActiveLinkSaved: ko.Computed<boolean>;
|
||||
|
||||
// Section-linking affects table if linkSrcSection is set. The controller value of the
|
||||
// link is the value of srcCol at activeRowId of linkSrcSection, or activeRowId itself when
|
||||
// srcCol is unset. If targetCol is set, we filter for all rows whose targetCol is equal to
|
||||
// the controller value. Otherwise, the controller value determines the rowId of the cursor.
|
||||
linkSrcSection: ko.Computed<ViewSectionRec>;
|
||||
linkSrcCol: ko.Computed<ColumnRec>;
|
||||
linkTargetCol: ko.Computed<ColumnRec>;
|
||||
|
||||
activeRowId: ko.Observable<RowId|null>; // May be null when there are no rows.
|
||||
|
||||
// If the view instance for section is instantiated, it will be accessible here.
|
||||
viewInstance: ko.Observable<BaseView|null>;
|
||||
|
||||
// Describes the most recent cursor position in the section. Only rowId and fieldIndex are used.
|
||||
lastCursorPos: CursorPos;
|
||||
|
||||
// Describes the most recent scroll position.
|
||||
lastScrollPos: {
|
||||
rowIndex: number; // Used for scrolly sections. Indicates the index of the first visible row.
|
||||
offset: number; // Pixel distance past the top of row indicated by rowIndex.
|
||||
scrollLeft: number; // Used for grid sections. Indicates the scrollLeft value of the scroll pane.
|
||||
};
|
||||
|
||||
disableAddRemoveRows: ko.Computed<boolean>;
|
||||
|
||||
isSorted: ko.Computed<boolean>;
|
||||
disableDragRows: ko.Computed<boolean>;
|
||||
|
||||
// Save all filters of fields in the section.
|
||||
saveFilters(): Promise<void>;
|
||||
|
||||
// Revert all filters of fields in the section.
|
||||
revertFilters(): void;
|
||||
|
||||
// Clear and save all filters of fields in the section.
|
||||
clearFilters(): void;
|
||||
}
|
||||
|
||||
export interface CustomViewSectionDef {
|
||||
/**
|
||||
* The mode.
|
||||
*/
|
||||
mode: ko.Observable<"url"|"plugin">;
|
||||
/**
|
||||
* The url.
|
||||
*/
|
||||
url: ko.Observable<string>;
|
||||
/**
|
||||
* Access granted to url.
|
||||
*/
|
||||
access: ko.Observable<string>;
|
||||
/**
|
||||
* The plugin id.
|
||||
*/
|
||||
pluginId: ko.Observable<string>;
|
||||
/**
|
||||
* The section id.
|
||||
*/
|
||||
sectionId: ko.Observable<string>;
|
||||
}
|
||||
|
||||
|
||||
export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel): void {
|
||||
this.viewFields = recordSet(this, docModel.viewFields, 'parentId', {sortBy: 'parentPos'});
|
||||
|
||||
const defaultOptions = {
|
||||
verticalGridlines: true,
|
||||
horizontalGridlines: true,
|
||||
zebraStripes: false,
|
||||
customView: '',
|
||||
};
|
||||
this.optionsObj = modelUtil.jsonObservable(this.options,
|
||||
(obj: any) => defaults(obj || {}, defaultOptions));
|
||||
|
||||
const customViewDefaults = {
|
||||
mode: 'url',
|
||||
url: '',
|
||||
access: '',
|
||||
pluginId: '',
|
||||
sectionId: ''
|
||||
};
|
||||
const customDefObj = modelUtil.jsonObservable(this.optionsObj.prop('customView'),
|
||||
(obj: any) => defaults(obj || {}, customViewDefaults));
|
||||
|
||||
this.customDef = {
|
||||
mode: customDefObj.prop('mode'),
|
||||
url: customDefObj.prop('url'),
|
||||
access: customDefObj.prop('access'),
|
||||
pluginId: customDefObj.prop('pluginId'),
|
||||
sectionId: customDefObj.prop('sectionId')
|
||||
};
|
||||
|
||||
this.themeDef = modelUtil.fieldWithDefault(this.theme, 'form');
|
||||
this.chartTypeDef = modelUtil.fieldWithDefault(this.chartType, 'bar');
|
||||
this.view = refRecord(docModel.views, this.parentId);
|
||||
|
||||
this.table = refRecord(docModel.tables, this.tableRef);
|
||||
|
||||
this.tableTitle = this.autoDispose(ko.pureComputed(() => this.table().tableTitle()));
|
||||
this.titleDef = modelUtil.fieldWithDefault(
|
||||
this.title,
|
||||
() => this.table().tableTitle() + (
|
||||
(this.parentKey() === 'record') ? '' : ` ${getWidgetTypes(this.parentKey.peek() as any).label}`
|
||||
)
|
||||
);
|
||||
|
||||
this.borderWidthPx = ko.pureComputed(function() { return this.borderWidth() + 'px'; }, this);
|
||||
|
||||
this.layoutSpecObj = modelUtil.jsonObservable(this.layoutSpec);
|
||||
|
||||
// Helper metadata item which indicates whether any of the section's fields have unsaved
|
||||
// changes to their filters. (True indicates unsaved changes)
|
||||
this.filterSpecChanged = Computed.create(this, use =>
|
||||
use(use(this.viewFields).getObservable()).some(field => !use(field.activeFilter.isSaved)));
|
||||
|
||||
this.filteredFields = Computed.create(this, use =>
|
||||
use(use(this.viewFields).getObservable()).filter(field => use(field.isFiltered)));
|
||||
|
||||
// Save all filters of fields in the section.
|
||||
this.saveFilters = () => {
|
||||
return docModel.docData.bundleActions(`Save all filters in ${this.titleDef()}`,
|
||||
async () => { await Promise.all(this.viewFields().all().map(field => field.activeFilter.save())); }
|
||||
);
|
||||
};
|
||||
|
||||
// Revert all filters of fields in the section.
|
||||
this.revertFilters = () => {
|
||||
this.viewFields().all().forEach(field => { field.activeFilter.revert(); });
|
||||
};
|
||||
|
||||
// Reset all filters of fields in the section to their default (i.e. unset) values.
|
||||
this.clearFilters = () => this.viewFields().all().forEach(field => field.activeFilter(''));
|
||||
|
||||
// Customizable version of the JSON-stringified sort spec. It may diverge from the saved one.
|
||||
this.activeSortJson = modelUtil.customValue(this.sortColRefs);
|
||||
|
||||
// This is an array (parsed from JSON) of colRefs (i.e. rowIds into the columns table), with a
|
||||
// twist: a rowId may be positive or negative, for ascending or descending respectively.
|
||||
// TODO: This method of ignoring columns which are deleted is inefficient and may cause conflicts
|
||||
// with sharing.
|
||||
this.activeSortSpec = modelUtil.jsonObservable(this.activeSortJson, (obj: any) => {
|
||||
return (obj || []).filter((sortRef: number) => {
|
||||
const colModel = docModel.columns.getRowModel(Math.abs(sortRef));
|
||||
return !colModel._isDeleted() && colModel.getRowId();
|
||||
});
|
||||
});
|
||||
|
||||
// Modified sort spec to take into account any active display columns.
|
||||
this.activeDisplaySortSpec = this.autoDispose(ko.computed(() => {
|
||||
return this.activeSortSpec().map(directionalColRef => {
|
||||
const colRef = Math.abs(directionalColRef);
|
||||
const field = this.viewFields().all().find(f => f.column().origColRef() === colRef);
|
||||
const effectiveColRef = field ? field.displayColRef() : colRef;
|
||||
return directionalColRef > 0 ? effectiveColRef : -effectiveColRef;
|
||||
});
|
||||
}));
|
||||
|
||||
// Evaluates to an array of column models, which are not referenced by anything in viewFields.
|
||||
this.hiddenColumns = this.autoDispose(ko.pureComputed(() => {
|
||||
const included = new Set(this.viewFields().all().map((f) => f.column().origColRef()));
|
||||
return this.table().columns().all().filter(function(col) {
|
||||
return !included.has(col.getRowId()) && !col.isHiddenCol();
|
||||
});
|
||||
}));
|
||||
|
||||
this.hasFocus = ko.pureComputed({
|
||||
// Read may occur for recently disposed sections, must check condition first.
|
||||
read: () => !this.isDisposed() && this.view().activeSectionId() === this.id() && !this.view().isLinking(),
|
||||
write: (val) => { if (val) { this.view().activeSectionId(this.id()); } }
|
||||
});
|
||||
|
||||
this.activeLinkSrcSectionRef = modelUtil.customValue(this.linkSrcSectionRef);
|
||||
this.activeLinkSrcColRef = modelUtil.customValue(this.linkSrcColRef);
|
||||
this.activeLinkTargetColRef = modelUtil.customValue(this.linkTargetColRef);
|
||||
|
||||
// Whether current linking state is as saved. It may be different during editing.
|
||||
this.isActiveLinkSaved = this.autoDispose(ko.pureComputed(() =>
|
||||
this.activeLinkSrcSectionRef.isSaved() &&
|
||||
this.activeLinkSrcColRef.isSaved() &&
|
||||
this.activeLinkTargetColRef.isSaved()));
|
||||
|
||||
// Section-linking affects this table if linkSrcSection is set. The controller value of the
|
||||
// link is the value of srcCol at activeRowId of linkSrcSection, or activeRowId itself when
|
||||
// srcCol is unset. If targetCol is set, we filter for all rows whose targetCol is equal to
|
||||
// the controller value. Otherwise, the controller value determines the rowId of the cursor.
|
||||
this.linkSrcSection = refRecord(docModel.viewSections, this.activeLinkSrcSectionRef);
|
||||
this.linkSrcCol = refRecord(docModel.columns, this.activeLinkSrcColRef);
|
||||
this.linkTargetCol = refRecord(docModel.columns, this.activeLinkTargetColRef);
|
||||
|
||||
this.activeRowId = ko.observable();
|
||||
|
||||
// If the view instance for this section is instantiated, it will be accessible here.
|
||||
this.viewInstance = ko.observable(null);
|
||||
|
||||
// Describes the most recent cursor position in the section.
|
||||
this.lastCursorPos = {
|
||||
rowId: 0,
|
||||
fieldIndex: 0
|
||||
};
|
||||
|
||||
// Describes the most recent scroll position.
|
||||
this.lastScrollPos = {
|
||||
rowIndex: 0, // Used for scrolly sections. Indicates the index of the first visible row.
|
||||
offset: 0, // Pixel distance past the top of row indicated by rowIndex.
|
||||
scrollLeft: 0 // Used for grid sections. Indicates the scrollLeft value of the scroll pane.
|
||||
};
|
||||
|
||||
this.disableAddRemoveRows = ko.pureComputed(() => this.table().disableAddRemoveRows());
|
||||
|
||||
this.isSorted = ko.pureComputed(() => this.activeSortSpec().length > 0);
|
||||
this.disableDragRows = ko.pureComputed(() => this.isSorted() || !this.table().supportsManualSort());
|
||||
}
|
||||
Reference in New Issue
Block a user