gristlabs_grist-core/app/client/components/LinkingState.ts

622 lines
34 KiB
TypeScript
Raw Normal View History

import {SequenceNEVER, SequenceNum} from "app/client/components/Cursor";
import {DataRowModel} from "app/client/models/DataRowModel";
import DataTableModel from "app/client/models/DataTableModel";
import {DocModel} from 'app/client/models/DocModel';
(core) Add other direction of linking by reflist Summary: Allows selecting by a reflist in another table. This generalises cursor-linking with a ref column, but now it's filter linking. Added another case to LinkingState where the source column is a reflist to the target table, filtering by the id column. Updated convertQueryFromRefs and related functions to handle this since the id column has no column ref. In this case the string 'id' is used instead of a number. LinkingState also checks if the source value is a reflist and uses that as the list of filter values instead of a single-element list of the cell value. Indirect linking also works, where the source and target columns both are both references to the same table. This was the plan for a source reflist and target ref column. I was surprised to see it also works perfectly when both columns are reflists, and it filters rows where there's an intersection! Adding rows to the target section using the selected source record for default values is iffy. When filtering by row ID, there's no column for defaults, so the new row disappears. For a source reflist and target ref, the first value of the reflist is the default, which is okayish. When both are reflists, the full source reflist is the default for the target column. This seems like a bit much but just using the first value seems a bit arbitrary when there's room for all of them? While doing all this I noticed an unrelated bug which I fixed as I was refactoring. Previously cursor linking based on a reference column did not update the cursor in the link target when the value of the selected reference cell changed. Now cursor linking uses a floating row model like most other cases to observe the value correctly. Test Plan: Extended SelectByRefList test and fixture, added previously failing test to RightPanelSelectBy. Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D3004
2021-08-30 13:29:39 +00:00
import {ColumnRec} from "app/client/models/entities/ColumnRec";
import {TableRec} from "app/client/models/entities/TableRec";
import {ViewSectionRec} from "app/client/models/entities/ViewSectionRec";
import {LinkConfig} from "app/client/ui/selectBy";
import {FilterColValues, QueryOperation} from "app/common/ActiveDocAPI";
import {isList, isListType, isRefListType} from "app/common/gristTypes";
import * as gutil from "app/common/gutil";
import {UIRowId} from 'app/plugin/GristAPI';
import {CellValue} from "app/plugin/GristData";
import {encodeObject} from 'app/plugin/objtypes';
import {Disposable, Holder, MultiHolder} from "grainjs";
import * as ko from "knockout";
import merge = require('lodash/merge');
import mapValues = require('lodash/mapValues');
import pick = require('lodash/pick');
import pickBy = require('lodash/pickBy');
// Descriptive string enum for each case of linking
// Currently used for rendering user-facing link info
// TODO JV: Eventually, switching the main block of linking logic in LinkingState constructor to be a big
// switch(linkType){} would make things cleaner.
// TODO JV: also should add "Custom-widget-linked" to this, but holding off until Jarek's changes land
type LinkType = "Filter:Summary-Group" |
"Filter:Col->Col"|
"Filter:Row->Col"|
"Summary"|
"Show-Referenced-Records"|
"Cursor:Same-Table"|
"Cursor:Reference"|
"Error:Invalid";
// If this LinkingState represents a filter link, it will set its filterState to this object
// The filterColValues portion is just the data needed for filtering (same as manual filtering), and is passed
// to the backend in some cases (CSV export)
// The filterState includes extra info to display filter state to the user
type FilterState = FilterColValues & {
filterLabels: { [colId: string]: string[] }; //formatted and displayCol-ed values to show to user
colTypes: {[colId: string]: string;}
};
function FilterStateToColValues(fs: FilterState) { return pick(fs, ['filters', 'operations']); }
//Since we're not making full objects for these, need to define sensible "empty" values here
export const EmptyFilterState: FilterState = {filters: {}, filterLabels: {}, operations: {}, colTypes: {}};
export const EmptyFilterColValues: FilterColValues = FilterStateToColValues(EmptyFilterState);
export class LinkingState extends Disposable {
(core) Add other direction of linking by reflist Summary: Allows selecting by a reflist in another table. This generalises cursor-linking with a ref column, but now it's filter linking. Added another case to LinkingState where the source column is a reflist to the target table, filtering by the id column. Updated convertQueryFromRefs and related functions to handle this since the id column has no column ref. In this case the string 'id' is used instead of a number. LinkingState also checks if the source value is a reflist and uses that as the list of filter values instead of a single-element list of the cell value. Indirect linking also works, where the source and target columns both are both references to the same table. This was the plan for a source reflist and target ref column. I was surprised to see it also works perfectly when both columns are reflists, and it filters rows where there's an intersection! Adding rows to the target section using the selected source record for default values is iffy. When filtering by row ID, there's no column for defaults, so the new row disappears. For a source reflist and target ref, the first value of the reflist is the default, which is okayish. When both are reflists, the full source reflist is the default for the target column. This seems like a bit much but just using the first value seems a bit arbitrary when there's room for all of them? While doing all this I noticed an unrelated bug which I fixed as I was refactoring. Previously cursor linking based on a reference column did not update the cursor in the link target when the value of the selected reference cell changed. Now cursor linking uses a floating row model like most other cases to observe the value correctly. Test Plan: Extended SelectByRefList test and fixture, added previously failing test to RightPanelSelectBy. Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D3004
2021-08-30 13:29:39 +00:00
// If linking affects target section's cursor, this will be a computed for the cursor rowId.
// Is undefined if not cursor-linked
public readonly cursorPos?: ko.Computed<UIRowId|null>;
// Cursor-links can be cyclic, need to keep track of both rowId and the lastCursorEdit that it came from to
// resolve it correctly, (use just one observable so they update at the same time)
//NOTE: observables don't do deep-equality check, so need to replace the whole array when updating
public readonly incomingCursorPos: ko.Computed<[UIRowId|null, SequenceNum]>;
(core) Add other direction of linking by reflist Summary: Allows selecting by a reflist in another table. This generalises cursor-linking with a ref column, but now it's filter linking. Added another case to LinkingState where the source column is a reflist to the target table, filtering by the id column. Updated convertQueryFromRefs and related functions to handle this since the id column has no column ref. In this case the string 'id' is used instead of a number. LinkingState also checks if the source value is a reflist and uses that as the list of filter values instead of a single-element list of the cell value. Indirect linking also works, where the source and target columns both are both references to the same table. This was the plan for a source reflist and target ref column. I was surprised to see it also works perfectly when both columns are reflists, and it filters rows where there's an intersection! Adding rows to the target section using the selected source record for default values is iffy. When filtering by row ID, there's no column for defaults, so the new row disappears. For a source reflist and target ref, the first value of the reflist is the default, which is okayish. When both are reflists, the full source reflist is the default for the target column. This seems like a bit much but just using the first value seems a bit arbitrary when there's room for all of them? While doing all this I noticed an unrelated bug which I fixed as I was refactoring. Previously cursor linking based on a reference column did not update the cursor in the link target when the value of the selected reference cell changed. Now cursor linking uses a floating row model like most other cases to observe the value correctly. Test Plan: Extended SelectByRefList test and fixture, added previously failing test to RightPanelSelectBy. Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D3004
2021-08-30 13:29:39 +00:00
// If linking affects filtering, this is a computed for the current filtering state, including user-facing
// labels for filter values and types of the filtered columns
// with a dependency on srcSection.activeRowId()
// Is undefined if not link-filtered
public readonly filterState?: ko.Computed<FilterState>;
// filterColValues is a subset of the current filterState needed for filtering (subset of ClientQuery)
// {[colId]: colValues, [colId]: operations} mapping,
(core) Add other direction of linking by reflist Summary: Allows selecting by a reflist in another table. This generalises cursor-linking with a ref column, but now it's filter linking. Added another case to LinkingState where the source column is a reflist to the target table, filtering by the id column. Updated convertQueryFromRefs and related functions to handle this since the id column has no column ref. In this case the string 'id' is used instead of a number. LinkingState also checks if the source value is a reflist and uses that as the list of filter values instead of a single-element list of the cell value. Indirect linking also works, where the source and target columns both are both references to the same table. This was the plan for a source reflist and target ref column. I was surprised to see it also works perfectly when both columns are reflists, and it filters rows where there's an intersection! Adding rows to the target section using the selected source record for default values is iffy. When filtering by row ID, there's no column for defaults, so the new row disappears. For a source reflist and target ref, the first value of the reflist is the default, which is okayish. When both are reflists, the full source reflist is the default for the target column. This seems like a bit much but just using the first value seems a bit arbitrary when there's room for all of them? While doing all this I noticed an unrelated bug which I fixed as I was refactoring. Previously cursor linking based on a reference column did not update the cursor in the link target when the value of the selected reference cell changed. Now cursor linking uses a floating row model like most other cases to observe the value correctly. Test Plan: Extended SelectByRefList test and fixture, added previously failing test to RightPanelSelectBy. Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D3004
2021-08-30 13:29:39 +00:00
public readonly filterColValues?: ko.Computed<FilterColValues>;
// Get default values for a new record so that it continues to satisfy the current linking filters
public readonly getDefaultColValues: () => any;
// Which case of linking we've got, this is a descriptive string-enum.
public readonly linkTypeDescription: ko.Computed<LinkType>;
private _docModel: DocModel;
private _srcSection: ViewSectionRec;
(core) Add other direction of linking by reflist Summary: Allows selecting by a reflist in another table. This generalises cursor-linking with a ref column, but now it's filter linking. Added another case to LinkingState where the source column is a reflist to the target table, filtering by the id column. Updated convertQueryFromRefs and related functions to handle this since the id column has no column ref. In this case the string 'id' is used instead of a number. LinkingState also checks if the source value is a reflist and uses that as the list of filter values instead of a single-element list of the cell value. Indirect linking also works, where the source and target columns both are both references to the same table. This was the plan for a source reflist and target ref column. I was surprised to see it also works perfectly when both columns are reflists, and it filters rows where there's an intersection! Adding rows to the target section using the selected source record for default values is iffy. When filtering by row ID, there's no column for defaults, so the new row disappears. For a source reflist and target ref, the first value of the reflist is the default, which is okayish. When both are reflists, the full source reflist is the default for the target column. This seems like a bit much but just using the first value seems a bit arbitrary when there's room for all of them? While doing all this I noticed an unrelated bug which I fixed as I was refactoring. Previously cursor linking based on a reference column did not update the cursor in the link target when the value of the selected reference cell changed. Now cursor linking uses a floating row model like most other cases to observe the value correctly. Test Plan: Extended SelectByRefList test and fixture, added previously failing test to RightPanelSelectBy. Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D3004
2021-08-30 13:29:39 +00:00
private _srcTableModel: DataTableModel;
private _srcColId: string | undefined;
constructor(docModel: DocModel, linkConfig: LinkConfig) {
super();
(core) Add other direction of linking by reflist Summary: Allows selecting by a reflist in another table. This generalises cursor-linking with a ref column, but now it's filter linking. Added another case to LinkingState where the source column is a reflist to the target table, filtering by the id column. Updated convertQueryFromRefs and related functions to handle this since the id column has no column ref. In this case the string 'id' is used instead of a number. LinkingState also checks if the source value is a reflist and uses that as the list of filter values instead of a single-element list of the cell value. Indirect linking also works, where the source and target columns both are both references to the same table. This was the plan for a source reflist and target ref column. I was surprised to see it also works perfectly when both columns are reflists, and it filters rows where there's an intersection! Adding rows to the target section using the selected source record for default values is iffy. When filtering by row ID, there's no column for defaults, so the new row disappears. For a source reflist and target ref, the first value of the reflist is the default, which is okayish. When both are reflists, the full source reflist is the default for the target column. This seems like a bit much but just using the first value seems a bit arbitrary when there's room for all of them? While doing all this I noticed an unrelated bug which I fixed as I was refactoring. Previously cursor linking based on a reference column did not update the cursor in the link target when the value of the selected reference cell changed. Now cursor linking uses a floating row model like most other cases to observe the value correctly. Test Plan: Extended SelectByRefList test and fixture, added previously failing test to RightPanelSelectBy. Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D3004
2021-08-30 13:29:39 +00:00
const {srcSection, srcCol, srcColId, tgtSection, tgtCol, tgtColId} = linkConfig;
this._docModel = docModel;
this._srcSection = srcSection;
(core) Add other direction of linking by reflist Summary: Allows selecting by a reflist in another table. This generalises cursor-linking with a ref column, but now it's filter linking. Added another case to LinkingState where the source column is a reflist to the target table, filtering by the id column. Updated convertQueryFromRefs and related functions to handle this since the id column has no column ref. In this case the string 'id' is used instead of a number. LinkingState also checks if the source value is a reflist and uses that as the list of filter values instead of a single-element list of the cell value. Indirect linking also works, where the source and target columns both are both references to the same table. This was the plan for a source reflist and target ref column. I was surprised to see it also works perfectly when both columns are reflists, and it filters rows where there's an intersection! Adding rows to the target section using the selected source record for default values is iffy. When filtering by row ID, there's no column for defaults, so the new row disappears. For a source reflist and target ref, the first value of the reflist is the default, which is okayish. When both are reflists, the full source reflist is the default for the target column. This seems like a bit much but just using the first value seems a bit arbitrary when there's room for all of them? While doing all this I noticed an unrelated bug which I fixed as I was refactoring. Previously cursor linking based on a reference column did not update the cursor in the link target when the value of the selected reference cell changed. Now cursor linking uses a floating row model like most other cases to observe the value correctly. Test Plan: Extended SelectByRefList test and fixture, added previously failing test to RightPanelSelectBy. Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D3004
2021-08-30 13:29:39 +00:00
this._srcColId = srcColId;
this._srcTableModel = docModel.dataTables[srcSection.table().tableId()];
(core) Add other direction of linking by reflist Summary: Allows selecting by a reflist in another table. This generalises cursor-linking with a ref column, but now it's filter linking. Added another case to LinkingState where the source column is a reflist to the target table, filtering by the id column. Updated convertQueryFromRefs and related functions to handle this since the id column has no column ref. In this case the string 'id' is used instead of a number. LinkingState also checks if the source value is a reflist and uses that as the list of filter values instead of a single-element list of the cell value. Indirect linking also works, where the source and target columns both are both references to the same table. This was the plan for a source reflist and target ref column. I was surprised to see it also works perfectly when both columns are reflists, and it filters rows where there's an intersection! Adding rows to the target section using the selected source record for default values is iffy. When filtering by row ID, there's no column for defaults, so the new row disappears. For a source reflist and target ref, the first value of the reflist is the default, which is okayish. When both are reflists, the full source reflist is the default for the target column. This seems like a bit much but just using the first value seems a bit arbitrary when there's room for all of them? While doing all this I noticed an unrelated bug which I fixed as I was refactoring. Previously cursor linking based on a reference column did not update the cursor in the link target when the value of the selected reference cell changed. Now cursor linking uses a floating row model like most other cases to observe the value correctly. Test Plan: Extended SelectByRefList test and fixture, added previously failing test to RightPanelSelectBy. Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D3004
2021-08-30 13:29:39 +00:00
const srcTableData = this._srcTableModel.tableData;
// === IMPORTANT NOTE! (this applies throughout this file)
// srcCol and tgtCol can be the "empty column"
// - emptyCol.getRowId() === 0
// - emptyCol.colId() === undefined
// The typical pattern to deal with this is to use `srcColId = col?.colId()`, and test for `if (srcColId) {...}`
this.linkTypeDescription = this.autoDispose(ko.computed((): LinkType => {
if (srcSection.isDisposed()) {
//srcSection disposed can happen transiently. Can happen when deleting tables and then undoing?
//nbrowser tests: LinkingErrors and RawData seem to hit this case
console.warn("srcSection disposed in linkingState: linkTypeDescription");
return "Error:Invalid";
}
if (srcSection.table().summarySourceTable() && srcColId === "group") {
return "Filter:Summary-Group"; //implemented as col->col, but special-cased in select-by
} else if (srcColId && tgtColId) {
return "Filter:Col->Col";
} else if (!srcColId && tgtColId) {
return "Filter:Row->Col";
} else if (srcColId && !tgtColId) { // Col->Row, i.e. show a ref
if (isRefListType(srcCol.type())) // TODO: fix this once ref-links are unified, both could be show-ref-rec
{ return "Show-Referenced-Records"; }
else
{ return "Cursor:Reference"; }
} else if (!srcColId && !tgtColId) { //Either same-table cursor link OR summary link
if (isSummaryOf(srcSection.table(), tgtSection.table()))
{ return "Summary"; }
else
{ return "Cursor:Same-Table"; }
} else { // This case shouldn't happen, but just check to be safe
return "Error:Invalid";
}
}));
if (srcSection.selectedRowsActive()) { // old, special-cased custom filter
const operation = (tgtColId && isRefListType(tgtCol.type())) ? 'intersects' : 'in';
this.filterState = this._srcCustomFilter(tgtCol, operation); // works whether tgtCol is the empty col or not
} else if (tgtColId) { // Standard filter link
// If srcCol is the empty col, is a row->col filter (i.e. id -> tgtCol)
// else is a col->col filter (srcCol -> tgtCol)
// MakeFilterObs handles it either way
this.filterState = this._makeFilterObs(srcCol, tgtCol);
} else if (srcColId && isRefListType(srcCol.type())) { // "Show Referenced Records" link
// tgtCol is the emptycol (i.e. the id col)
// srcCol must be a reference to the tgt table
// Link will filter tgt section to show exactly the set of rowIds referenced by the srcCol
// (NOTE: currently we only do this for reflists, single refs handled as cursor links for now)
this.filterState = this._makeFilterObs(srcCol, undefined);
} else if (!srcColId && isSummaryOf(srcSection.table(), tgtSection.table())) { //Summary linking
// We do summary filtering if no cols specified and summary section is linked to a more detailed summary
// (or to the summarySource table)
// Implemented as multiple column filters, one for each groupByCol of the src table
// temp vars for _update to use (can't set filterState directly since it's gotta be a computed)
const _filterState = ko.observable<FilterState>();
this.filterState = this.autoDispose(ko.computed(() => _filterState()));
// update may be called multiple times, so need a holder to handle disposal
// Note: grainjs MultiHolder can't actually be cleared. To be able to dispose of multiple things, we need
// to make a MultiHolder in a Holder, which feels ugly but works.
// TODO: Update this if we ever patch grainjs to allow multiHolder.clear()
const updateHolder = Holder.create(this);
// source data table could still be loading (this could happen after changing the group-by
// columns of a linked summary table for instance). Define an _update function to be called when data loads
const _update = () => {
if (srcSection.isDisposed() || srcSection.table().groupByColumns().length === 0) {
// srcSection disposed can happen transiently. Can happen when deleting tables and then undoing?
// Tests nbrowser/LinkingErrors and RawData might hit this case
// groupByColumns === [] can happen if we make a summary tab [group by nothing]. (in which case: don't filter)
_filterState(EmptyFilterState);
return;
}
//Make a MultiHolder to own this invocation's objects (disposes of old one)
//TODO (MultiHolder in a Holder is a bit of a hack, but needed to hold multiple objects I think)
const updateMultiHolder = MultiHolder.create(updateHolder);
//Make one filter for each groupBycolumn of srcSection
const resultFilters: (ko.Computed<FilterState>|undefined)[] = srcSection.table().groupByColumns().map(srcGCol =>
this._makeFilterObs(srcGCol, summaryGetCorrespondingCol(srcGCol, tgtSection.table()), updateMultiHolder)
);
//If any are undef (i.e. error in makeFilterObs), error out
if(resultFilters.some((f) => f === undefined)) {
console.warn("LINKINGSTATE: some of filters are undefined", resultFilters);
_filterState(EmptyFilterState);
return;
}
//Merge them together in a computed
const resultComputed = updateMultiHolder.autoDispose(ko.computed(() => {
return merge({}, ...resultFilters.map(filtObs => filtObs!())) as FilterState;
}));
_filterState(resultComputed());
resultComputed.subscribe((val) => _filterState(val));
}; // End of update function
// Call update when data loads, also call now to be safe
this.autoDispose(srcTableData.dataLoadedEmitter.addListener(_update));
_update();
// ================ CURSOR LINKS: =================
} else { //!tgtCol && !summary-link && (!lookup-link || !reflist),
// either same-table cursor-link (!srcCol && !tgtCol, so do activeRowId -> cursorPos)
// or cursor-link by reference ( srcCol && !tgtCol, so do srcCol -> cursorPos)
// Cursor linking notes:
//
// If multiple viewSections are cursor-linked together A->B->C, we need to propagate the linked cursorPos along.
// The old way was to have: A.activeRowId -> (sets by cursor-link) -> B.activeRowId, and so on
// |
// --> [B.LS] --> [C.LS] |
// / | B.LS.cursorPos / | C.LS.cursorPos |
// / v / v |
// [ A ]--------/ [ B ] --------------/ [ C ] |
// A.actRowId B.actRowId |
//
// However, if e.g. viewSec B is filtered, the correct rowId might not exist in B, and so its activeRowId would be
// on a different row, and therefore the cursor linking would set C to a different row from A, even if it existed
// in C
//
// Normally this wouldn't be too bad, but to implement bidirectional linking requires allowing cycles of
// cursor-links, in which case this behavior becomes extra-problematic, both in being more unexpected from a UX
// perspective and because a section will eventually be linked to itself, which is an unstable loop.
//
// A better solution is to propagate the linked rowId directly through the chain of linkingStates without passing
// through the activeRowIds of the sections, so whether a section is filtered or not doesn't affect propagation.
//
// B.LS.incCursPos |
// --> [B.LS] --------------> [C.LS] |
// / | | |
// / v B.LS.cursorPos v C.LS.cursorPos |
// [ A ]--------/ [ B ] [ C ] |
// A.actRowId |
//
// If the previous section has a linkingState, we use the previous LS's incomingCursorPos
// (i.e. two sections back) instead of looking at our srcSection's activeRowId. This way it doesn't matter how
// section B is filtered, since we're getting our cursorPos straight from A (through a computed in B.LS)
//
// However, each linkingState needs to decide whether to use the cursorPos from the srcSec (i.e. its activeRowId),
// or to use the previous linkState's incomingCursorPos. We want to use whichever section the user most recently
// interacted with, i.e. whichever cursor update was most recent. For this we use, the cursor version (given in
// viewSection.lastCursorEdit). incomingCursorPos is a pair of [rowId, sequenceNum], so each linkingState sets its
// incomingCursorPos to whichever is most recent between its srcSection, and the previous LS's incCursPos.
//
// If we do this right, the end result is that because the lastCursorEdits are guaranteed to be unique,
// there is always a stable configuration of links, where even in the case of a cycle the incomingCursorPos-es
// will all take their rowId and version from the most recently edited viewSection in the cycle,
// which is what the user expects
//
// ...from C--> [A.LS] --------> [B.LS] --> [C.LS] ----->...to A |
// | | / | |
// v v / v |
// [ A ] [ B ] ---------/ [ C ] |
// (most recently edited) |
//
// Once the incomingCursorPos-es are determined correctly, the cursorPos-es just need to pull out the rowId,
// and that will drive the cursors of the associated tgt section for each LS.
//
// NOTE: setting cursorPos *WILL* change the viewSections' cursor, but it's special-cased to
// so that cursor-driven linking doesn't modify their lastCursorEdit times, so that lastCursorEdit
// reflects only changes driven by external factors
// (e.g. page load, user moving cursor, user changing linking settings/filter settings)
// =============================
// gets the relevant col value for the passed-in rowId, or return rowId unchanged if same-table link
const srcValueFunc = this._makeValGetter(this._srcSection.table(), this._srcColId);
// check for failure
if (srcValueFunc) {
//Incoming-cursor-pos determines what the linked cursor position should be, considering the previous
//linked section (srcSection) and all upstream sections (through srcSection.linkingState)
this.incomingCursorPos = this.autoDispose((ko.computed(() => {
// NOTE: This computed primarily decides between srcSec and prevLink. Here's what those mean:
// e.g. consider sections A->B->C, (where this === C)
// We need to decide between taking cursor info from B, our srcSection (1 hop back)
// vs taking cursor info from further back, e.g. A, or before (2+ hops back)
// To take cursor info from further back, we rely on B's linkingState, since B's linkingState will
// be looking at the preceding sections, either A or whatever is behind A.
// Therefore: we either use srcSection (1 back), or prevLink = srcSection.linkingState (2+ back)
// Get srcSection's info (1 hop back)
const srcSecPos = this._srcSection.activeRowId.peek(); //we don't depend on this, only on its cursor version
const srcSecVersion = this._srcSection.lastCursorEdit();
// If cursors haven't been initialized, cursor-linking doesn't make sense, so don't do it
if(srcSecVersion === SequenceNEVER) {
return [null, SequenceNEVER] as [UIRowId|null, SequenceNum];
}
// Get previous linkingstate's info, if applicable (2 or more hops back)
const prevLink = this._srcSection.linkingState?.();
const prevLinkHasCursor = prevLink?.incomingCursorPos &&
(prevLink.linkTypeDescription() === "Cursor:Same-Table" ||
prevLink.linkTypeDescription() === "Cursor:Reference");
const [prevLinkedPos, prevLinkedVersion] = prevLinkHasCursor ? prevLink.incomingCursorPos() :
[null, SequenceNEVER];
// ==== Determine whose info to use:
// If prevLinkedVersion < srcSecVersion, then the prev linked data is stale, don't use it
// If prevLinkedVersion == srcSecVersion, then srcSec is the driver for this link cycle (i.e. we're its first
// outgoing link), AND the link cycle has come all the way around
const usePrev = prevLinkHasCursor && prevLinkedVersion > srcSecVersion;
// srcSec/prevLinkedPos is rowId from srcSec. However if "Cursor:Reference", we must follow the ref in srcCol
// srcValueFunc will get the appropriate value based on this._srcColId if that's the case
const tgtCursorPos = (srcValueFunc(usePrev ? prevLinkedPos : srcSecPos) || "new") as UIRowId;
// NOTE: srcValueFunc returns 'null' if rowId is the add-row, so we coerce that back into || "new"
// NOTE: cursor linking is only ever done by the id column (for same-table) or by single Ref col (cursor:ref),
// so we'll never have to worry about `null` showing up as an actual cell-value. (A blank Ref is just `0`)
return [
tgtCursorPos,
usePrev ? prevLinkedVersion : srcSecVersion, //propagate which version our cursorPos is from
] as [UIRowId|null, SequenceNum];
})));
// Pull out just the rowId from incomingCursor Pos
// (This get applied directly to tgtSection's cursor),
this.cursorPos = this.autoDispose(ko.computed(() => this.incomingCursorPos()[0]));
(core) Add other direction of linking by reflist Summary: Allows selecting by a reflist in another table. This generalises cursor-linking with a ref column, but now it's filter linking. Added another case to LinkingState where the source column is a reflist to the target table, filtering by the id column. Updated convertQueryFromRefs and related functions to handle this since the id column has no column ref. In this case the string 'id' is used instead of a number. LinkingState also checks if the source value is a reflist and uses that as the list of filter values instead of a single-element list of the cell value. Indirect linking also works, where the source and target columns both are both references to the same table. This was the plan for a source reflist and target ref column. I was surprised to see it also works perfectly when both columns are reflists, and it filters rows where there's an intersection! Adding rows to the target section using the selected source record for default values is iffy. When filtering by row ID, there's no column for defaults, so the new row disappears. For a source reflist and target ref, the first value of the reflist is the default, which is okayish. When both are reflists, the full source reflist is the default for the target column. This seems like a bit much but just using the first value seems a bit arbitrary when there's room for all of them? While doing all this I noticed an unrelated bug which I fixed as I was refactoring. Previously cursor linking based on a reference column did not update the cursor in the link target when the value of the selected reference cell changed. Now cursor linking uses a floating row model like most other cases to observe the value correctly. Test Plan: Extended SelectByRefList test and fixture, added previously failing test to RightPanelSelectBy. Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D3004
2021-08-30 13:29:39 +00:00
}
if (!srcColId) { // If same-table cursor-link, copy getDefaultColValues from the source if possible
const getDefaultColValues = srcSection.linkingState()?.getDefaultColValues;
if (getDefaultColValues) {
this.getDefaultColValues = getDefaultColValues;
}
}
}
// ======= End of cursor linking
// Make filterColValues, which is just the filtering-relevant parts of filterState
// (it's used in places that don't need the user-facing labels, e.g. CSV export)
this.filterColValues = (this.filterState) ?
ko.computed(() => FilterStateToColValues(this.filterState!()))
: undefined;
if (!this.getDefaultColValues) {
this.getDefaultColValues = () => {
if (!this.filterState) {
return {};
}
const {filters, operations} = this.filterState.peek();
return mapValues(
pickBy(filters, (value: any[], key: string) => value.length > 0 && key !== "id"),
(value, key) => operations[key] === "intersects" ? encodeObject(value) : value[0]
);
};
}
}
/**
* Returns a boolean indicating whether editing should be disabled in the destination section.
*/
public disableEditing(): boolean {
if (!this.filterState) {
return false;
}
const srcRowId = this._srcSection.activeRowId();
return srcRowId === 'new' || srcRowId === null;
}
(core) Add other direction of linking by reflist Summary: Allows selecting by a reflist in another table. This generalises cursor-linking with a ref column, but now it's filter linking. Added another case to LinkingState where the source column is a reflist to the target table, filtering by the id column. Updated convertQueryFromRefs and related functions to handle this since the id column has no column ref. In this case the string 'id' is used instead of a number. LinkingState also checks if the source value is a reflist and uses that as the list of filter values instead of a single-element list of the cell value. Indirect linking also works, where the source and target columns both are both references to the same table. This was the plan for a source reflist and target ref column. I was surprised to see it also works perfectly when both columns are reflists, and it filters rows where there's an intersection! Adding rows to the target section using the selected source record for default values is iffy. When filtering by row ID, there's no column for defaults, so the new row disappears. For a source reflist and target ref, the first value of the reflist is the default, which is okayish. When both are reflists, the full source reflist is the default for the target column. This seems like a bit much but just using the first value seems a bit arbitrary when there's room for all of them? While doing all this I noticed an unrelated bug which I fixed as I was refactoring. Previously cursor linking based on a reference column did not update the cursor in the link target when the value of the selected reference cell changed. Now cursor linking uses a floating row model like most other cases to observe the value correctly. Test Plan: Extended SelectByRefList test and fixture, added previously failing test to RightPanelSelectBy. Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D3004
2021-08-30 13:29:39 +00:00
/**
* Makes a standard filter link (summary tables and cursor links handled separately)
* treats (srcCol === undefined) as srcColId === "id", same for tgt
*
* if srcColId === "id", uses src activeRowId as the selector value (i.e. a ref to that row)
* else, gets the current value in selectedRow's SrcCol
*
* Returns a FilterColValues with a single filter {[tgtColId|"id":string] : (selectorVals:val[])}
* note: selectorVals is always a list of values: if reflist the leading "L" is trimmed, if single val then [val]
*
* If unable to initialize (sometimes happens when things are loading?), returns undefined
*
* NOTE: srcColId and tgtColId MUST NOT both be undefined, that implies either cursor linking or summary linking,
* which this doesn't handle
*
* @param srcCol srcCol for the filter, or undefined/the empty column to mean the entire record
* @param tgtCol tgtCol for the filter, or undefined/the empty column to mean the entire record
* @param [owner=this] Owner for all created disposables
* @private
*/
private _makeFilterObs(
srcCol: ColumnRec|undefined,
tgtCol: ColumnRec|undefined,
owner: MultiHolder = this): ko.Computed<FilterState> | undefined
{
const srcColId = srcCol?.colId();
const tgtColId = tgtCol?.colId();
//Assert: if both are null then it's a summary filter or same-table cursor-link, neither of which should go here
if(!srcColId && !tgtColId) {
throw Error("ERROR in _makeFilterObs: srcCol and tgtCol can't both be empty");
}
//if (srcCol), selectorVal is the value in activeRowId[srcCol].
//if (!srcCol), then selectorVal is the entire record, so func just returns the rowId, or null if the rowId is "new"
const selectorValGetter = this._makeValGetter(this._srcSection.table(), srcColId);
// Figure out display val to show for the selector (if selector is a Ref)
// - if srcCol is a ref, we display its displayColModel(), which is what is shown in the cell
// - However, if srcColId === 'id', there is no srcCol.displayColModel.
// We also can't use tgtCol.displayColModel, since we're getting values from the source section.
// Therefore: The value we want to display is srcRow[tgtCol.visibleColModel.colId]
//
// Note: if we've gotten here, tgtCol is guaranteed to be a ref/reflist if srcColId === undefined
// (because we ruled out the undef/undef case above)
// Note: tgtCol.visibleCol.colId can be undefined, iff visibleCol is rowId. makeValGetter handles that implicitly
const displayColId = srcColId ?
srcCol!.displayColModel().colId() :
tgtCol!.visibleColModel().colId();
const displayValGetter = this._makeValGetter(this._srcSection.table(), displayColId);
//Note: if src is a reflist, its displayVal will be a list of the visibleCol vals,
// i.e ["L", visVal1, visVal2], but they won't be formatter()-ed
//Grab the formatter (for numerics, dates, etc)
const displayValFormatter = srcColId ? srcCol!.visibleColFormatter() : tgtCol!.visibleColFormatter();
const isSrcRefList = srcColId && isRefListType(srcCol!.type());
const isTgtRefList = tgtColId && isRefListType(tgtCol!.type());
if (!selectorValGetter || !displayValGetter) {
console.error("ERROR in _makeFilterObs: couldn't create valGetters for srcSection");
return undefined;
}
//Now, create the actual observable that updates with activeRowId
//(we autodispose/return it at the end of the function) is this right? TODO JV
return owner.autoDispose(ko.computed(() => {
if (this._srcSection.isDisposed()) {
//srcSection disposed can happen transiently. Can happen when deleting tables and then undoing?
//nbrowser tests: LinkingErrors and RawData seem to hit this case
console.warn("srcSection disposed in LinkingState._makeFilterObs");
return EmptyFilterState;
}
if (this._srcSection.isDisposed()) {
//happened transiently in test: "RawData should remove all tables except one (...)"
console.warn("LinkingState._makeFilterObs: srcSectionDisposed");
return EmptyFilterState;
}
//Get selector-rowId
(core) Add other direction of linking by reflist Summary: Allows selecting by a reflist in another table. This generalises cursor-linking with a ref column, but now it's filter linking. Added another case to LinkingState where the source column is a reflist to the target table, filtering by the id column. Updated convertQueryFromRefs and related functions to handle this since the id column has no column ref. In this case the string 'id' is used instead of a number. LinkingState also checks if the source value is a reflist and uses that as the list of filter values instead of a single-element list of the cell value. Indirect linking also works, where the source and target columns both are both references to the same table. This was the plan for a source reflist and target ref column. I was surprised to see it also works perfectly when both columns are reflists, and it filters rows where there's an intersection! Adding rows to the target section using the selected source record for default values is iffy. When filtering by row ID, there's no column for defaults, so the new row disappears. For a source reflist and target ref, the first value of the reflist is the default, which is okayish. When both are reflists, the full source reflist is the default for the target column. This seems like a bit much but just using the first value seems a bit arbitrary when there's room for all of them? While doing all this I noticed an unrelated bug which I fixed as I was refactoring. Previously cursor linking based on a reference column did not update the cursor in the link target when the value of the selected reference cell changed. Now cursor linking uses a floating row model like most other cases to observe the value correctly. Test Plan: Extended SelectByRefList test and fixture, added previously failing test to RightPanelSelectBy. Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D3004
2021-08-30 13:29:39 +00:00
const srcRowId = this._srcSection.activeRowId();
//Get values from selector row
const selectorCellVal = selectorValGetter(srcRowId);
const displayCellVal = displayValGetter(srcRowId);
// Coerce values into lists (FilterColValues wants output as a list, even if only 1 val)
let filterValues: any[];
let displayValues: any[];
if(!isSrcRefList) {
filterValues = [selectorCellVal];
displayValues = [displayCellVal];
} else if(isSrcRefList && isList(selectorCellVal)) { //Reflists are: ["L", ref1, ref2, ...], slice off the L
filterValues = selectorCellVal.slice(1);
//selectorValue and displayValue might not match up? Shouldn't happen, but let's yell loudly if it does
if (isList(displayCellVal) && displayCellVal.length === selectorCellVal.length) {
displayValues = displayCellVal.slice(1);
(core) Add other direction of linking by reflist Summary: Allows selecting by a reflist in another table. This generalises cursor-linking with a ref column, but now it's filter linking. Added another case to LinkingState where the source column is a reflist to the target table, filtering by the id column. Updated convertQueryFromRefs and related functions to handle this since the id column has no column ref. In this case the string 'id' is used instead of a number. LinkingState also checks if the source value is a reflist and uses that as the list of filter values instead of a single-element list of the cell value. Indirect linking also works, where the source and target columns both are both references to the same table. This was the plan for a source reflist and target ref column. I was surprised to see it also works perfectly when both columns are reflists, and it filters rows where there's an intersection! Adding rows to the target section using the selected source record for default values is iffy. When filtering by row ID, there's no column for defaults, so the new row disappears. For a source reflist and target ref, the first value of the reflist is the default, which is okayish. When both are reflists, the full source reflist is the default for the target column. This seems like a bit much but just using the first value seems a bit arbitrary when there's room for all of them? While doing all this I noticed an unrelated bug which I fixed as I was refactoring. Previously cursor linking based on a reference column did not update the cursor in the link target when the value of the selected reference cell changed. Now cursor linking uses a floating row model like most other cases to observe the value correctly. Test Plan: Extended SelectByRefList test and fixture, added previously failing test to RightPanelSelectBy. Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D3004
2021-08-30 13:29:39 +00:00
} else {
console.warn("Error in LinkingState: displayVal list doesn't match selectorVal list ");
displayValues = filterValues; //fallback to unformatted values
(core) Add other direction of linking by reflist Summary: Allows selecting by a reflist in another table. This generalises cursor-linking with a ref column, but now it's filter linking. Added another case to LinkingState where the source column is a reflist to the target table, filtering by the id column. Updated convertQueryFromRefs and related functions to handle this since the id column has no column ref. In this case the string 'id' is used instead of a number. LinkingState also checks if the source value is a reflist and uses that as the list of filter values instead of a single-element list of the cell value. Indirect linking also works, where the source and target columns both are both references to the same table. This was the plan for a source reflist and target ref column. I was surprised to see it also works perfectly when both columns are reflists, and it filters rows where there's an intersection! Adding rows to the target section using the selected source record for default values is iffy. When filtering by row ID, there's no column for defaults, so the new row disappears. For a source reflist and target ref, the first value of the reflist is the default, which is okayish. When both are reflists, the full source reflist is the default for the target column. This seems like a bit much but just using the first value seems a bit arbitrary when there's room for all of them? While doing all this I noticed an unrelated bug which I fixed as I was refactoring. Previously cursor linking based on a reference column did not update the cursor in the link target when the value of the selected reference cell changed. Now cursor linking uses a floating row model like most other cases to observe the value correctly. Test Plan: Extended SelectByRefList test and fixture, added previously failing test to RightPanelSelectBy. Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D3004
2021-08-30 13:29:39 +00:00
}
} else { //isSrcRefList && !isList(val), probably null. Happens with blank reflists, or if cursor on the 'new' row
filterValues = [];
displayValues = [];
if(selectorCellVal !== null) { // should be null, but let's warn if it's not
console.warn("Error in LinkingState.makeFilterObs(), srcVal is reflist but has non-list non-null value");
}
}
// ==== Determine operation to use for filter ====
// Common case: use 'in' for single vals, or 'intersects' for ChoiceLists & RefLists
let operation = (tgtColId && isListType(tgtCol!.type())) ? 'intersects' : 'in';
// # Special case 1:
// Blank selector shouldn't mean "show no records", it should mean "show records where tgt column is also blank"
// This is the default behavior for single-ref -> single-ref links
// However, if tgtCol is a list and the selectorVal is blank/empty, the default behavior ([] intersects tgtlist)
// doesn't work, we need to explicitly specify the operation to be 'empty', to select empty cells
if (tgtCol?.type() === "ChoiceList" && !isSrcRefList && selectorCellVal === "") { operation = 'empty'; }
else if (isTgtRefList && !isSrcRefList && selectorCellVal === 0) { operation = 'empty'; }
else if (isTgtRefList && isSrcRefList && filterValues.length === 0) { operation = 'empty'; }
// Note, we check each case separately since they have different "blank" values"
// Other types can have different falsey values when non-blank (e.g. a Ref=0 is a blank cell, but for numbers,
// 0 would be a valid value, and to check for an empty number-cell you'd check for null)
// However, we don't need to check for those here, since they can't be linked to list types
// NOTES ON CHOICELISTS: they only show up in a few cases.
// - ChoiceList can only ever appear in links as the tgtcol
// (ChoiceLists can only be linked from summ. tables, and summary flattens lists, so srcCol would be 'Choice')
// - empty Choice is [""].
// # Special case 2:
// If tgtCol is a single ref, blankness is represented by [0]
// However if srcCol is a RefList, blankness is represented by [], which won't match the [0].
// We create the 0 explicitly so the filter will select the blank Refs
else if (!isTgtRefList && isSrcRefList && filterValues.length === 0) {
filterValues = [0];
displayValues = [''];
}
// # Special case 3:
// If the srcSection has no row selected (cursor on the add-row, or no data in srcSection), we should
// show no rows in tgtSection. (we also gray it out and show the "No row selected in $SRCSEC" msg)
// This should line up with when this.disableEditing() returns true
if (srcRowId === 'new' || srcRowId === null) {
operation = 'in';
filterValues = [];
displayValues = [];
}
// Run values through formatters (for dates, numerics, Refs with visCol = rowId)
const filterLabelVals: string[] = displayValues.map(v => displayValFormatter.formatAny(v));
return {
filters: {[tgtColId || "id"]: filterValues},
filterLabels: {[tgtColId || "id"]: filterLabelVals},
operations: {[tgtColId || "id"]: operation},
colTypes: {[tgtColId || "id"]: (tgtCol || srcCol)!.type()}
//at least one of tgt/srcCol is guaranteed to be non-null, and they will have the same type
} as FilterState;
}));
(core) Add other direction of linking by reflist Summary: Allows selecting by a reflist in another table. This generalises cursor-linking with a ref column, but now it's filter linking. Added another case to LinkingState where the source column is a reflist to the target table, filtering by the id column. Updated convertQueryFromRefs and related functions to handle this since the id column has no column ref. In this case the string 'id' is used instead of a number. LinkingState also checks if the source value is a reflist and uses that as the list of filter values instead of a single-element list of the cell value. Indirect linking also works, where the source and target columns both are both references to the same table. This was the plan for a source reflist and target ref column. I was surprised to see it also works perfectly when both columns are reflists, and it filters rows where there's an intersection! Adding rows to the target section using the selected source record for default values is iffy. When filtering by row ID, there's no column for defaults, so the new row disappears. For a source reflist and target ref, the first value of the reflist is the default, which is okayish. When both are reflists, the full source reflist is the default for the target column. This seems like a bit much but just using the first value seems a bit arbitrary when there's room for all of them? While doing all this I noticed an unrelated bug which I fixed as I was refactoring. Previously cursor linking based on a reference column did not update the cursor in the link target when the value of the selected reference cell changed. Now cursor linking uses a floating row model like most other cases to observe the value correctly. Test Plan: Extended SelectByRefList test and fixture, added previously failing test to RightPanelSelectBy. Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D3004
2021-08-30 13:29:39 +00:00
}
// Value for this.filterColValues based on the values in srcSection.selectedRows
//"null" for column implies id column
private _srcCustomFilter(
column: ColumnRec|undefined, operation: QueryOperation): ko.Computed<FilterState> {
//Note: column may be the empty column, i.e. column != undef, but column.colId() is undefined
const colId = (!column || column.colId() === undefined) ? "id" : column.colId();
return this.autoDispose(ko.computed(() => {
const values = this._srcSection.selectedRows();
return {
filters: {[colId]: values},
filterLabels: {[colId]: values?.map(v => String(v))}, //selectedRows should never be null if customFiltered
operations: {[colId]: operation},
colTypes: {[colId]: column?.type() || `Ref:${column?.table().tableId}`}
} as FilterState; //TODO: fix this once we have cases of customwidget linking to test with
}));
}
// Returns a ValGetter function, i.e. (rowId) => cellValue(rowId, colId), for the specified table and colId,
// Or null if there's an error in making the valgetter
// Note:
// - Uses a row model to create a dependency on the cell's value, so changes to the cell value will notify observers
// - ValGetter returns null for the 'new' row
// - An undefined colId means to use the 'id' column, i.e. Valgetter is (rowId)=>rowId
private _makeValGetter(table: TableRec, colId: string | undefined, owner: MultiHolder=this)
: ( null | ((r: UIRowId | null) => CellValue | null) ) // (null | ValGetter)
{
if(colId === undefined) { //passthrough for id cols
return (rowId: UIRowId | null) => { return rowId === 'new' ? null : rowId; };
}
const tableModel = this._docModel.dataTables[table.tableId()];
const rowModel = (tableModel.createFloatingRowModel()) as DataRowModel;
owner.autoDispose(rowModel);
const cellObs = rowModel.cells[colId];
// If no cellObs, can't make a val getter. This shouldn't happen, but may happen
(core) Add other direction of linking by reflist Summary: Allows selecting by a reflist in another table. This generalises cursor-linking with a ref column, but now it's filter linking. Added another case to LinkingState where the source column is a reflist to the target table, filtering by the id column. Updated convertQueryFromRefs and related functions to handle this since the id column has no column ref. In this case the string 'id' is used instead of a number. LinkingState also checks if the source value is a reflist and uses that as the list of filter values instead of a single-element list of the cell value. Indirect linking also works, where the source and target columns both are both references to the same table. This was the plan for a source reflist and target ref column. I was surprised to see it also works perfectly when both columns are reflists, and it filters rows where there's an intersection! Adding rows to the target section using the selected source record for default values is iffy. When filtering by row ID, there's no column for defaults, so the new row disappears. For a source reflist and target ref, the first value of the reflist is the default, which is okayish. When both are reflists, the full source reflist is the default for the target column. This seems like a bit much but just using the first value seems a bit arbitrary when there's room for all of them? While doing all this I noticed an unrelated bug which I fixed as I was refactoring. Previously cursor linking based on a reference column did not update the cursor in the link target when the value of the selected reference cell changed. Now cursor linking uses a floating row model like most other cases to observe the value correctly. Test Plan: Extended SelectByRefList test and fixture, added previously failing test to RightPanelSelectBy. Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D3004
2021-08-30 13:29:39 +00:00
// transiently while the separate linking-related observables get updated.
if (!cellObs) {
console.warn(`Issue in LinkingState._makeValGetter(${table.tableId()},${colId}): cellObs is nullish`);
(core) Add other direction of linking by reflist Summary: Allows selecting by a reflist in another table. This generalises cursor-linking with a ref column, but now it's filter linking. Added another case to LinkingState where the source column is a reflist to the target table, filtering by the id column. Updated convertQueryFromRefs and related functions to handle this since the id column has no column ref. In this case the string 'id' is used instead of a number. LinkingState also checks if the source value is a reflist and uses that as the list of filter values instead of a single-element list of the cell value. Indirect linking also works, where the source and target columns both are both references to the same table. This was the plan for a source reflist and target ref column. I was surprised to see it also works perfectly when both columns are reflists, and it filters rows where there's an intersection! Adding rows to the target section using the selected source record for default values is iffy. When filtering by row ID, there's no column for defaults, so the new row disappears. For a source reflist and target ref, the first value of the reflist is the default, which is okayish. When both are reflists, the full source reflist is the default for the target column. This seems like a bit much but just using the first value seems a bit arbitrary when there's room for all of them? While doing all this I noticed an unrelated bug which I fixed as I was refactoring. Previously cursor linking based on a reference column did not update the cursor in the link target when the value of the selected reference cell changed. Now cursor linking uses a floating row model like most other cases to observe the value correctly. Test Plan: Extended SelectByRefList test and fixture, added previously failing test to RightPanelSelectBy. Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D3004
2021-08-30 13:29:39 +00:00
return null;
}
return (rowId: UIRowId | null) => { // returns cellValue | null
rowModel.assign(rowId);
if (rowId === 'new') { return null; } // used to return "new", hopefully the change doesn't come back to haunt us
return cellObs();
(core) Add other direction of linking by reflist Summary: Allows selecting by a reflist in another table. This generalises cursor-linking with a ref column, but now it's filter linking. Added another case to LinkingState where the source column is a reflist to the target table, filtering by the id column. Updated convertQueryFromRefs and related functions to handle this since the id column has no column ref. In this case the string 'id' is used instead of a number. LinkingState also checks if the source value is a reflist and uses that as the list of filter values instead of a single-element list of the cell value. Indirect linking also works, where the source and target columns both are both references to the same table. This was the plan for a source reflist and target ref column. I was surprised to see it also works perfectly when both columns are reflists, and it filters rows where there's an intersection! Adding rows to the target section using the selected source record for default values is iffy. When filtering by row ID, there's no column for defaults, so the new row disappears. For a source reflist and target ref, the first value of the reflist is the default, which is okayish. When both are reflists, the full source reflist is the default for the target column. This seems like a bit much but just using the first value seems a bit arbitrary when there's room for all of them? While doing all this I noticed an unrelated bug which I fixed as I was refactoring. Previously cursor linking based on a reference column did not update the cursor in the link target when the value of the selected reference cell changed. Now cursor linking uses a floating row model like most other cases to observe the value correctly. Test Plan: Extended SelectByRefList test and fixture, added previously failing test to RightPanelSelectBy. Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D3004
2021-08-30 13:29:39 +00:00
};
}
}
// === Helpers:
/**
* Returns whether 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()));
}
/**
* When TableA is a summary of TableB, each of TableA.groupByCols corresponds to a specific col of TableB
* This function returns the column of B that corresponds to a particular groupByCol of A
* - If A is a direct summary of B, then the corresponding col for A.someCol is A.someCol.summarySource()
* - However if A and B are both summaries of C, then A.someCol.summarySource() would
* give us C.someCol, but what we actually want is B.someCol.
* - Since we know A is a summary of B, then B's groupByCols must include all of A's groupbycols,
* so we can get B.someCol by matching on colId.
* @param srcGBCol: ColumnRec, must be a groupByColumn, and srcGBCol.table() must be a summary of tgtTable
* @param tgtTable: TableRec to get corresponding column from
* @returns {ColumnRec} The corresponding column of tgtTable
*/
function summaryGetCorrespondingCol(srcGBCol: ColumnRec, tgtTable: TableRec): ColumnRec {
if(!isSummaryOf(srcGBCol.table(), tgtTable))
{ throw Error("ERROR in LinkingState summaryGetCorrespondingCol: srcTable must be summary of tgtTable"); }
if(tgtTable.summarySourceTable() === 0) { //if direct summary
return srcGBCol.summarySource();
} else { // else summary->summary, match by colId
const srcColId = srcGBCol.colId();
const retVal = tgtTable.groupByColumns().find((tgtCol) => tgtCol.colId() === srcColId); //should always exist
if(!retVal) { throw Error("ERROR in LinkingState summaryGetCorrespondingCol: summary table lacks groupby col"); }
return retVal;
}
}