mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Port LinkingState.js to TypeScript
Summary: Converted LinkingState from constructor function to class. Test Plan: no Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2997
This commit is contained in:
parent
faa0d9988e
commit
7465af8ce8
@ -14,8 +14,8 @@ var Base = require('./Base');
|
|||||||
var {Cursor} = require('./Cursor');
|
var {Cursor} = require('./Cursor');
|
||||||
var FieldBuilder = require('../widgets/FieldBuilder');
|
var FieldBuilder = require('../widgets/FieldBuilder');
|
||||||
var commands = require('./commands');
|
var commands = require('./commands');
|
||||||
var LinkingState = require('./LinkingState');
|
|
||||||
var BackboneEvents = require('backbone').Events;
|
var BackboneEvents = require('backbone').Events;
|
||||||
|
const {LinkingState} = require('./LinkingState');
|
||||||
const {ClientColumnGetters} = require('app/client/models/ClientColumnGetters');
|
const {ClientColumnGetters} = require('app/client/models/ClientColumnGetters');
|
||||||
const {reportError, UserError} = require('app/client/models/errors');
|
const {reportError, UserError} = require('app/client/models/errors');
|
||||||
const {urlState} = require('app/client/models/gristUrlState');
|
const {urlState} = require('app/client/models/gristUrlState');
|
||||||
@ -131,13 +131,12 @@ function BaseView(gristDoc, viewSectionModel, options) {
|
|||||||
this._linkingState = this.autoDispose(koUtil.computedBuilder(() => {
|
this._linkingState = this.autoDispose(koUtil.computedBuilder(() => {
|
||||||
let v = this.viewSection;
|
let v = this.viewSection;
|
||||||
let src = v.linkSrcSection();
|
let src = v.linkSrcSection();
|
||||||
const filterByAllShown = v.optionsObj.prop('filterByAllShown');
|
|
||||||
if (!src.getRowId()) {
|
if (!src.getRowId()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const config = new LinkConfig(v);
|
const config = new LinkConfig(v);
|
||||||
return LinkingState.create.bind(LinkingState, this.gristDoc, config, filterByAllShown());
|
return LinkingState.create.bind(LinkingState, null, this.gristDoc, config);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(`Can't create LinkingState: ${err.message}`);
|
console.warn(`Can't create LinkingState: ${err.message}`);
|
||||||
return null;
|
return null;
|
||||||
|
@ -1,145 +0,0 @@
|
|||||||
const _ = require('underscore');
|
|
||||||
const ko = require('knockout');
|
|
||||||
const dispose = require('../lib/dispose');
|
|
||||||
const gutil = require('app/common/gutil');
|
|
||||||
const {isRefListType} = require("app/common/gristTypes");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns if the first table is a summary of the second. If both are summary tables, returns true
|
|
||||||
* if the second table is a more detailed summary, i.e. has additional group-by columns.
|
|
||||||
* @param {MetaRowModel} summary: RowModel for the table to check for being the summary table.
|
|
||||||
* @param {MetaRowModel} detail: RowModel for the table to check for being the detailed version.
|
|
||||||
* @returns {Boolean} Whether the first argument is a summarized version of the second.
|
|
||||||
*/
|
|
||||||
function isSummaryOf(summary, detail) {
|
|
||||||
let summarySource = summary.summarySourceTable();
|
|
||||||
if (summarySource === detail.getRowId()) { return true; }
|
|
||||||
let detailSource = detail.summarySourceTable();
|
|
||||||
return (summarySource &&
|
|
||||||
detailSource === summarySource &&
|
|
||||||
summary.getRowId() !== detail.getRowId() &&
|
|
||||||
gutil.isSubset(summary.summarySourceColRefs(), detail.summarySourceColRefs()));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maintains state useful for linking sections, i.e. auto-filtering and auto-scrolling.
|
|
||||||
* Exposes .filterColValues, which is either null or a computed evaluating to a filtering object;
|
|
||||||
* and .cursorPos, which is either null or a computed that evaluates to a cursor position.
|
|
||||||
* LinkingState must be created with a valid srcSection and tgtSection.
|
|
||||||
*
|
|
||||||
* There are several modes of linking:
|
|
||||||
* (1) If tgtColId is set, tgtSection will be filtered to show rows whose values of target column
|
|
||||||
* are equal to the value of source column in srcSection at the cursor. With byAllShown set, all
|
|
||||||
* values in srcSection are used (rather than only the value in the cursor).
|
|
||||||
* (2) If srcSection is a summary of tgtSection, then tgtSection is filtered to show only those
|
|
||||||
* rows that match the row at the cursor of srcSection.
|
|
||||||
* (3) If tgtColId is null, tgtSection is scrolled to the rowId determined by the value of the
|
|
||||||
* source column at the cursor in srcSection.
|
|
||||||
*
|
|
||||||
* @param gristDoc: GristDoc instance, for getting the relevant TableData objects.
|
|
||||||
* @param srcSection: RowModel for the section that drives the target section.
|
|
||||||
* @param srcColId: Name of the column that drives the target section, or null to use rowId.
|
|
||||||
* @param tgtSection: RowModel for the section that's being driven.
|
|
||||||
* @param tgtColId: Name of the reference column to auto-filter by, or null to auto-scroll.
|
|
||||||
* @param byAllShown: For auto-filter, filter by all values in srcSection rather than only the
|
|
||||||
* value at the cursor. The user can use column filters on srcSection to control what's shown
|
|
||||||
* in the linked tgtSection.
|
|
||||||
*/
|
|
||||||
function LinkingState(gristDoc, linkConfig, byAllShown) {
|
|
||||||
const {srcSection, srcColId, tgtSection, tgtCol, tgtColId} = linkConfig;
|
|
||||||
this._srcSection = srcSection;
|
|
||||||
|
|
||||||
let srcTableModel = gristDoc.getTableModel(srcSection.table().tableId());
|
|
||||||
let srcTableData = srcTableModel.tableData;
|
|
||||||
|
|
||||||
// Function from srcRowId (i.e. srcSection.activeRowId()) to the source value. It is used for
|
|
||||||
// filtering or for cursor positioning, depending on the setting of tgtCol.
|
|
||||||
let srcValueFunc = srcColId ? srcTableData.getRowPropFunc(srcColId) : _.identity;
|
|
||||||
|
|
||||||
// If linking affects target section's cursor, this will be a computed for the cursor rowId.
|
|
||||||
this.cursorPos = null;
|
|
||||||
|
|
||||||
// If linking affects filtering, this is a computed for the current filtering state, as a
|
|
||||||
// {[colId]: colValues} mapping, with a dependency on srcSection.activeRowId(). Otherwise, null.
|
|
||||||
this.filterColValues = null;
|
|
||||||
|
|
||||||
// A computed that evaluates to a filter function to use, or null if not filtering. If
|
|
||||||
// filtering, depends on srcSection.activeRowId().
|
|
||||||
if (tgtColId) {
|
|
||||||
const operations = {[tgtColId]: isRefListType(tgtCol.type()) ? 'intersects' : 'in'};
|
|
||||||
if (byAllShown) {
|
|
||||||
// (This is legacy code that isn't currently reachable)
|
|
||||||
// Include all values present in srcSection.
|
|
||||||
this.filterColValues = this.autoDispose(ko.computed(() => {
|
|
||||||
const srcValues = new Set();
|
|
||||||
const viewInstance = srcSection.viewInstance();
|
|
||||||
if (viewInstance) {
|
|
||||||
for (const srcRowId of viewInstance.sortedRows.getKoArray().all()) {
|
|
||||||
if (srcRowId !== 'new') {
|
|
||||||
srcValues.add(srcValueFunc(srcRowId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {filters: {[tgtColId]: Array.from(srcValues)}};
|
|
||||||
}));
|
|
||||||
} else if (srcColId) {
|
|
||||||
let srcRowModel = this.autoDispose(srcTableModel.createFloatingRowModel());
|
|
||||||
let srcCell = srcRowModel.cells[srcColId];
|
|
||||||
// If no srcCell, linking is broken; do nothing. This shouldn't happen, but may happen
|
|
||||||
// transiently while the separate linking-related observables get updated.
|
|
||||||
if (srcCell) {
|
|
||||||
this.filterColValues = this.autoDispose(ko.computed(() => {
|
|
||||||
const srcRowId = srcSection.activeRowId();
|
|
||||||
srcRowModel.assign(srcRowId);
|
|
||||||
return {filters: {[tgtColId]: [srcCell()]}, operations};
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.filterColValues = this.autoDispose(ko.computed(() => {
|
|
||||||
const srcRowId = srcSection.activeRowId();
|
|
||||||
return {filters: {[tgtColId]: [srcRowId]}, operations};
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
} else if (isSummaryOf(srcSection.table(), tgtSection.table())) {
|
|
||||||
// We filter summary tables when a summary section is linked to a more detailed one without
|
|
||||||
// specifying src or target column. The filtering is on the shared group-by column (i.e. all
|
|
||||||
// those in the srcSection).
|
|
||||||
// TODO: This approach doesn't help cursor-linking (the other direction). If we have the
|
|
||||||
// inverse of summary-table's 'group' column, we could implement both, and more efficiently.
|
|
||||||
const isDirectSummary = srcSection.table().summarySourceTable() === tgtSection.table().getRowId();
|
|
||||||
this.filterColValues = this.autoDispose(ko.computed(() => {
|
|
||||||
const srcRowId = srcSection.activeRowId();
|
|
||||||
const filters = {};
|
|
||||||
const operations = {};
|
|
||||||
for (const c of srcSection.table().groupByColumns()) {
|
|
||||||
const col = c.summarySource();
|
|
||||||
const colId = col.colId();
|
|
||||||
const srcValue = srcTableData.getValue(srcRowId, colId);
|
|
||||||
filters[colId] = [srcValue];
|
|
||||||
if (isDirectSummary) {
|
|
||||||
const tgtColType = col.type();
|
|
||||||
if (tgtColType === 'ChoiceList' || tgtColType.startsWith('RefList:')) {
|
|
||||||
operations[colId] = 'intersects';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {filters, operations};
|
|
||||||
}));
|
|
||||||
} else if (isSummaryOf(tgtSection.table(), srcSection.table())) {
|
|
||||||
// TODO: We should move the cursor, but don't currently it for summaries. For that, we need a
|
|
||||||
// column or map representing the inverse of summary table's "group" column.
|
|
||||||
} else {
|
|
||||||
this.cursorPos = this.autoDispose(ko.computed(() => srcValueFunc(srcSection.activeRowId())));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dispose.makeDisposable(LinkingState);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a boolean indicating whether editing should be disabled in the destination section.
|
|
||||||
*/
|
|
||||||
LinkingState.prototype.disableEditing = function() {
|
|
||||||
return this.filterColValues && this._srcSection.activeRowId() === 'new';
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = LinkingState;
|
|
145
app/client/components/LinkingState.ts
Normal file
145
app/client/components/LinkingState.ts
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import {GristDoc} from "app/client/components/GristDoc";
|
||||||
|
import {DataRowModel} from "app/client/models/DataRowModel";
|
||||||
|
import {TableRec} from "app/client/models/entities/TableRec";
|
||||||
|
import {ViewSectionRec} from "app/client/models/entities/ViewSectionRec";
|
||||||
|
import {LinkConfig} from "app/client/ui/selectBy";
|
||||||
|
import {ClientQuery, QueryOperation} from "app/common/ActiveDocAPI";
|
||||||
|
import {isRefListType} from "app/common/gristTypes";
|
||||||
|
import * as gutil from "app/common/gutil";
|
||||||
|
import {Disposable} from "grainjs";
|
||||||
|
import * as ko from "knockout";
|
||||||
|
import * as _ from "underscore";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns if the first table is a summary of the second. If both are summary tables, returns true
|
||||||
|
* if the second table is a more detailed summary, i.e. has additional group-by columns.
|
||||||
|
* @param summary: TableRec for the table to check for being the summary table.
|
||||||
|
* @param detail: TableRec for the table to check for being the detailed version.
|
||||||
|
* @returns {Boolean} Whether the first argument is a summarized version of the second.
|
||||||
|
*/
|
||||||
|
function isSummaryOf(summary: TableRec, detail: TableRec): boolean {
|
||||||
|
const summarySource = summary.summarySourceTable();
|
||||||
|
if (summarySource === detail.getRowId()) { return true; }
|
||||||
|
const detailSource = detail.summarySourceTable();
|
||||||
|
return (Boolean(summarySource) &&
|
||||||
|
detailSource === summarySource &&
|
||||||
|
summary.getRowId() !== detail.getRowId() &&
|
||||||
|
gutil.isSubset(summary.summarySourceColRefs(), detail.summarySourceColRefs()));
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilterColValues = Pick<ClientQuery, "filters" | "operations">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maintains state useful for linking sections, i.e. auto-filtering and auto-scrolling.
|
||||||
|
* Exposes .filterColValues, which is either null or a computed evaluating to a filtering object;
|
||||||
|
* and .cursorPos, which is either null or a computed that evaluates to a cursor position.
|
||||||
|
* LinkingState must be created with a valid srcSection and tgtSection.
|
||||||
|
*
|
||||||
|
* There are several modes of linking:
|
||||||
|
* (1) If tgtColId is set, tgtSection will be filtered to show rows whose values of target column
|
||||||
|
* are equal to the value of source column in srcSection at the cursor. With byAllShown set, all
|
||||||
|
* values in srcSection are used (rather than only the value in the cursor).
|
||||||
|
* (2) If srcSection is a summary of tgtSection, then tgtSection is filtered to show only those
|
||||||
|
* rows that match the row at the cursor of srcSection.
|
||||||
|
* (3) If tgtColId is null, tgtSection is scrolled to the rowId determined by the value of the
|
||||||
|
* source column at the cursor in srcSection.
|
||||||
|
*
|
||||||
|
* @param gristDoc: GristDoc instance, for getting the relevant TableData objects.
|
||||||
|
* @param srcSection: RowModel for the section that drives the target section.
|
||||||
|
* @param srcColId: Name of the column that drives the target section, or null to use rowId.
|
||||||
|
* @param tgtSection: RowModel for the section that's being driven.
|
||||||
|
* @param tgtColId: Name of the reference column to auto-filter by, or null to auto-scroll.
|
||||||
|
* @param byAllShown: For auto-filter, filter by all values in srcSection rather than only the
|
||||||
|
* value at the cursor. The user can use column filters on srcSection to control what's shown
|
||||||
|
* in the linked tgtSection.
|
||||||
|
*/
|
||||||
|
export class LinkingState extends Disposable {
|
||||||
|
public readonly cursorPos: ko.Computed<number> | null;
|
||||||
|
public readonly filterColValues: ko.Computed<FilterColValues> | null;
|
||||||
|
private _srcSection: ViewSectionRec;
|
||||||
|
|
||||||
|
constructor(gristDoc: GristDoc, linkConfig: LinkConfig) {
|
||||||
|
super();
|
||||||
|
const {srcSection, srcColId, tgtSection, tgtCol, tgtColId} = linkConfig;
|
||||||
|
this._srcSection = srcSection;
|
||||||
|
|
||||||
|
const srcTableModel = gristDoc.getTableModel(srcSection.table().tableId());
|
||||||
|
const srcTableData = srcTableModel.tableData;
|
||||||
|
|
||||||
|
// Function from srcRowId (i.e. srcSection.activeRowId()) to the source value. It is used for
|
||||||
|
// filtering or for cursor positioning, depending on the setting of tgtCol.
|
||||||
|
const srcValueFunc = srcColId ? srcTableData.getRowPropFunc(srcColId)! : _.identity;
|
||||||
|
|
||||||
|
// If linking affects target section's cursor, this will be a computed for the cursor rowId.
|
||||||
|
this.cursorPos = null;
|
||||||
|
|
||||||
|
// If linking affects filtering, this is a computed for the current filtering state, as a
|
||||||
|
// {[colId]: colValues} mapping, with a dependency on srcSection.activeRowId(). Otherwise, null.
|
||||||
|
this.filterColValues = null;
|
||||||
|
|
||||||
|
// A computed that evaluates to a filter function to use, or null if not filtering. If
|
||||||
|
// filtering, depends on srcSection.activeRowId().
|
||||||
|
if (tgtColId) {
|
||||||
|
const operations = {[tgtColId]: isRefListType(tgtCol.type()) ? 'intersects' : 'in' as QueryOperation};
|
||||||
|
if (srcColId) {
|
||||||
|
const srcRowModel = this.autoDispose(srcTableModel.createFloatingRowModel()) as DataRowModel;
|
||||||
|
const srcCell = srcRowModel.cells[srcColId];
|
||||||
|
// If no srcCell, linking is broken; do nothing. This shouldn't happen, but may happen
|
||||||
|
// transiently while the separate linking-related observables get updated.
|
||||||
|
if (srcCell) {
|
||||||
|
this.filterColValues = this.autoDispose(ko.computed(() => {
|
||||||
|
const srcRowId = srcSection.activeRowId();
|
||||||
|
srcRowModel.assign(srcRowId);
|
||||||
|
return {filters: {[tgtColId]: [srcCell()]}, operations} as FilterColValues;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.filterColValues = this.autoDispose(ko.computed(() => {
|
||||||
|
const srcRowId = srcSection.activeRowId();
|
||||||
|
return {filters: {[tgtColId]: [srcRowId]}, operations} as FilterColValues;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} else if (isSummaryOf(srcSection.table(), tgtSection.table())) {
|
||||||
|
// We filter summary tables when a summary section is linked to a more detailed one without
|
||||||
|
// specifying src or target column. The filtering is on the shared group-by column (i.e. all
|
||||||
|
// those in the srcSection).
|
||||||
|
// TODO: This approach doesn't help cursor-linking (the other direction). If we have the
|
||||||
|
// inverse of summary-table's 'group' column, we could implement both, and more efficiently.
|
||||||
|
const isDirectSummary = srcSection.table().summarySourceTable() === tgtSection.table().getRowId();
|
||||||
|
this.filterColValues = this.autoDispose(ko.computed(() => {
|
||||||
|
const result: FilterColValues = {filters: {}, operations: {}};
|
||||||
|
const srcRowId = srcSection.activeRowId();
|
||||||
|
for (const c of srcSection.table().groupByColumns()) {
|
||||||
|
const col = c.summarySource();
|
||||||
|
const colId = col.colId();
|
||||||
|
const srcValue = srcTableData.getValue(srcRowId as number, colId);
|
||||||
|
result.filters[colId] = [srcValue];
|
||||||
|
if (isDirectSummary) {
|
||||||
|
const tgtColType = col.type();
|
||||||
|
if (tgtColType === 'ChoiceList' || tgtColType.startsWith('RefList:')) {
|
||||||
|
result.operations![colId] = 'intersects';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}));
|
||||||
|
} else if (isSummaryOf(tgtSection.table(), srcSection.table())) {
|
||||||
|
// TODO: We should move the cursor, but don't currently it for summaries. For that, we need a
|
||||||
|
// column or map representing the inverse of summary table's "group" column.
|
||||||
|
} else {
|
||||||
|
this.cursorPos = this.autoDispose(ko.computed(() =>
|
||||||
|
srcValueFunc(
|
||||||
|
srcSection.activeRowId() as number
|
||||||
|
) as number
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a boolean indicating whether editing should be disabled in the destination section.
|
||||||
|
*/
|
||||||
|
public disableEditing(): boolean {
|
||||||
|
return Boolean(this.filterColValues) && this._srcSection.activeRowId() === 'new';
|
||||||
|
}
|
||||||
|
}
|
2
app/client/declarations.d.ts
vendored
2
app/client/declarations.d.ts
vendored
@ -308,7 +308,7 @@ declare module "app/client/models/DataTableModel" {
|
|||||||
constructor(docModel: DocModel, tableData: TableData, tableMetaRow: TableRec);
|
constructor(docModel: DocModel, tableData: TableData, tableMetaRow: TableRec);
|
||||||
public createLazyRowsModel(sortedRowSet: SortedRowSet, optRowModelClass: any):
|
public createLazyRowsModel(sortedRowSet: SortedRowSet, optRowModelClass: any):
|
||||||
DataTableModel.LazyArrayModel<BaseRowModel>;
|
DataTableModel.LazyArrayModel<BaseRowModel>;
|
||||||
public createFloatingRowModel(optRowModelClass: any): BaseRowModel;
|
public createFloatingRowModel(optRowModelClass?: any): BaseRowModel;
|
||||||
}
|
}
|
||||||
export = DataTableModel;
|
export = DataTableModel;
|
||||||
}
|
}
|
||||||
|
@ -120,7 +120,7 @@ export class DataTableModelWithDiff extends DisposableWithEvents implements Data
|
|||||||
return this._wrappedModel.createLazyRowsModel(sortedRowSet, optRowModelClass);
|
return this._wrappedModel.createLazyRowsModel(sortedRowSet, optRowModelClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
public createFloatingRowModel(optRowModelClass: any): BaseRowModel {
|
public createFloatingRowModel(optRowModelClass?: any): BaseRowModel {
|
||||||
return this._wrappedModel.createFloatingRowModel(optRowModelClass);
|
return this._wrappedModel.createFloatingRowModel(optRowModelClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user