gristlabs_grist-core/app/client/models/entities/ViewRec.ts
Dmitry S 3fa5125cf7 (core) Highlight rows used as a selector in linking, but do not show 'inactive' cursors.
Summary:
1. Introduces another highlight for link-selector rows, with the same color as
   regular selection, and allowing to overlap with regular selection.
2. Don't show "secondary" cursors (those in inactive sections), to keep a single
   cursor on the screen, since having multiple (which different in color) could
   cause confusion.
3. An unrelated improvement (prompted by a new fixture doc) is to default the
   active section to the top-left one (rather than the one with smallest rowId).
4. Another unrelated improvement (prompted by a test affected by the previous unrelated improvement) is to skip chart widgets when searching (previously search would step through those with an invisible "cursor").

Includes also tweaks for better testing on Arm-based Macs:
- Add support for TEST_CHROME_BINARY_PATH environment variable (helpful for a Mac arm64 architecture workaround)
- Remove unsetting of SELENIUM_REMOTE_URL when running headless (unlikely to affect anyone, and can be done outside the script, but interferes with the Mac workaround)

Test Plan: Added a new test case that cursor and linking-selector CSS classes are present or absent appropriately. Fixed test affected by the fix to default active section.

Reviewers: georgegevoian

Reviewed By: georgegevoian

Differential Revision: https://phab.getgrist.com/D3891
2023-06-21 12:21:19 -04:00

86 lines
3.7 KiB
TypeScript

import {BoxSpec} from 'app/client/components/Layout';
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, 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>>;
tabBarItem: ko.Computed<KoArray<TabBarRec>>;
layoutSpecObj: modelUtil.SaveableObjObservable<BoxSpec>;
// An observable for the ref of the section last selected by the user.
activeSectionId: ko.Computed<number>;
// This is active collapsed section id. Set when the widget is clicked.
activeCollapsedSectionId: ko.Observable<number>;
// Saved collapsed sections.
collapsedSections: ko.Computed<number[]>;
// Active collapsed sections, changed by the user, can be different from the
// saved collapsed sections, for a brief moment (editor is buffering changes).
activeCollapsedSections: ko.Observable<number[]>;
activeSection: ko.Computed<ViewSectionRec>;
// If the active section is removed, set the next active section to be the default.
_isActiveSectionGone: ko.Computed<boolean>;
}
export function createViewRec(this: ViewRec, docModel: DocModel): void {
this.viewSections = recordSet(this, docModel.viewSections, 'parentId');
this.tabBarItem = recordSet(this, docModel.tabBar, 'viewRef');
this.layoutSpecObj = modelUtil.jsonObservable(this.layoutSpec);
this.activeCollapsedSectionId = ko.observable(0);
this.collapsedSections = this.autoDispose(ko.pureComputed(() => {
const allSections = new Set(this.viewSections().all().map(x => x.id()));
const collapsed: number[] = (this.layoutSpecObj().collapsed || []).map(x => x.leaf as number);
return collapsed.filter(x => allSections.has(x));
}));
this.activeCollapsedSections = ko.observable(this.collapsedSections.peek());
// 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.
// `!this.getRowId()` implies that this is an empty (non-existent) view record
// which happens when viewing the raw data tables, in which case the default is no active view section.
if (this.isDisposed() || !this.getRowId()) { return 0; }
const all = this.viewSections().all();
const collapsed = new Set(this.activeCollapsedSections());
const visible = all.filter(x => !collapsed.has(x.id()));
// Default to the first leaf from layoutSpec (which corresponds to the top-left section), or
// fall back to the first item in the list if anything goes wrong (previous behavior).
const firstLeaf = getFirstLeaf(this.layoutSpecObj.peek());
return visible.find(s => s.getRowId() === firstLeaf) ? firstLeaf as number :
(visible[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);
}
}));
}
function getFirstLeaf(layoutSpec: BoxSpec|undefined): BoxSpec['leaf'] {
while (layoutSpec?.children?.length) {
layoutSpec = layoutSpec.children[0];
}
return layoutSpec?.leaf;
}