diff --git a/app/client/components/GristDoc.ts b/app/client/components/GristDoc.ts index aada14ec..7aca22a1 100644 --- a/app/client/components/GristDoc.ts +++ b/app/client/components/GristDoc.ts @@ -61,7 +61,6 @@ import { CursorMonitor, ViewCursorPos } from "app/client/components/CursorMonito import { EditorMonitor } from "app/client/components/EditorMonitor"; import { FieldEditor } from "app/client/widgets/FieldEditor"; import { Drafts } from "app/client/components/Drafts"; -import {findIndex} from "lodash"; const G = getBrowserGlobals('document', 'window'); @@ -97,7 +96,6 @@ export class GristDoc extends DisposableWithEvents { public viewModel: ViewRec; public activeViewId: Computed; public currentPageName: Observable; - public showDocTourTable: boolean = false; public docData: DocData; public docInfo: DocInfoRec; public docPluginManager: DocPluginManager; @@ -168,11 +166,7 @@ export class GristDoc extends DisposableWithEvents { if (result === 'GristDocTour') { // GristDocTour is a special table that is usually hidden from users, but putting /p/GristDocTour // in the URL navigates to it and makes it visible in the list of pages in the sidebar - this.showDocTourTable = true; - result = findIndex(this.docModel.views.rowModels, view => view?.name.peek() === result); - if (result === -1) { - result = undefined; // not found, back to the default - } + result = this.docModel.views.tableData.findRow('name', result); } return result || use(defaultViewId); }); diff --git a/app/client/models/DocModel.ts b/app/client/models/DocModel.ts index 1caa2c8e..65876cff 100644 --- a/app/client/models/DocModel.ts +++ b/app/client/models/DocModel.ts @@ -18,6 +18,7 @@ import * as koArray from 'app/client/lib/koArray'; import * as koUtil from 'app/client/lib/koUtil'; import * as DataTableModel from 'app/client/models/DataTableModel'; import {DocData} from 'app/client/models/DocData'; +import {urlState} from 'app/client/models/gristUrlState'; import * as MetaRowModel from 'app/client/models/MetaRowModel'; import * as MetaTableModel from 'app/client/models/MetaTableModel'; import * as rowset from 'app/client/models/rowset'; @@ -123,11 +124,17 @@ export class DocModel { public dataTablesByRef = new Map(); public allTabs: KoArray = this.tabBar.createAllRowsModel('tabPos'); - public allDocPages: KoArray = this.pages.createAllRowsModel('pagePos'); + + // Excludes pages hidden by ACL rules or other reasons (e.g. doc-tour) + public visibleDocPages: ko.Computed; // Flag for tracking whether document is in formula-editing mode public editingFormula: ko.Observable = ko.observable(false); + // TODO This is a temporary solution until we expose creation of doc-tours to users. This flag + // is initialized once on page load. If set, then the tour page (if any) will be visible. + public showDocTourTable: boolean = (urlState().state.get().docPage === 'GristDocTour'); + // List of all the metadata tables. private _metaTables: Array>; @@ -158,6 +165,10 @@ export class DocModel { add: r => this._onAddTable(r), remove: r => this._onRemoveTable(r), }); + + // Get a list of only the visible pages. + const allPages = this.pages.createAllRowsModel('pagePos'); + this.visibleDocPages = ko.computed(() => allPages.all().filter(p => !p.isHidden())); } private _metaTableModel>( diff --git a/app/client/models/SearchModel.ts b/app/client/models/SearchModel.ts index 88ae690a..1b43d295 100644 --- a/app/client/models/SearchModel.ts +++ b/app/client/models/SearchModel.ts @@ -125,7 +125,7 @@ class FinderImpl implements IFinder { // Initialize the steppers. Returns false if anything goes wrong. public init(): boolean { - const pages: any[] = this._gristDoc.docModel.allDocPages.peek(); + const pages: any[] = this._gristDoc.docModel.visibleDocPages.peek(); this._pageStepper.array = pages; this._pageStepper.index = pages.findIndex(t => t.viewRef() === this._gristDoc.activeViewId.get()); if (this._pageStepper.index < 0) { return false; } diff --git a/app/client/models/TreeModel.ts b/app/client/models/TreeModel.ts index a25c468f..42e96174 100644 --- a/app/client/models/TreeModel.ts +++ b/app/client/models/TreeModel.ts @@ -13,13 +13,12 @@ */ import { insertPositions } from "app/client/lib/tableUtil"; -import { BulkColValues } from "app/common/DocActions"; +import { BulkColValues, UserAction } from "app/common/DocActions"; import { nativeCompare } from "app/common/gutil"; import { obsArray, ObsArray } from "grainjs"; import forEach = require("lodash/forEach"); import forEachRight = require("lodash/forEachRight"); import reverse = require("lodash/reverse"); -import { TableData } from "./TableData"; /** * A generic definition of a tree to use with the `TreeViewComponent`. The tree implements @@ -56,6 +55,12 @@ export interface TreeRecord { [key: string]: any; } +// This is compatible with TableData from app/client/models/TableData. +export interface TreeTableData { + getRecords(): TreeRecord[]; + sendTableActions(actions: UserAction[]): Promise; +} + // describes a function that builds dom for a particular record type DomBuilder = (id: number) => HTMLElement; @@ -63,8 +68,8 @@ type DomBuilder = (id: number) => HTMLElement; // Returns a list of the records from table that is suitable to build the tree model, ie: records // are sorted by .posKey, and .indentation starts at 0 for the first records and can only increase // one step at a time (but can decrease as much as you want). -function getRecords(table: TableData) { - const records = (table.getRecords() as TreeRecord[]) +function getRecords(table: TreeTableData) { + const records = table.getRecords() .sort((a, b) => nativeCompare(a.pagePos, b.pagePos)); return fixIndents(records); } @@ -83,7 +88,7 @@ export function fixIndents(records: TreeRecord[]) { // build a tree model from a grist table storing tree view data -export function fromTableData(table: TableData, buildDom: DomBuilder, oldModel?: TreeModelRecord) { +export function fromTableData(table: TreeTableData, buildDom: DomBuilder, oldModel?: TreeModelRecord) { const records = getRecords(table); const storage = {table, records}; @@ -113,7 +118,7 @@ export function fromTableData(table: TableData, buildDom: DomBuilder, oldModel?: // a table data with all of its records as returned by getRecords(tableData) interface Storage { - table: TableData; + table: TreeTableData; records: TreeRecord[]; } diff --git a/app/client/models/entities/DocInfoRec.ts b/app/client/models/entities/DocInfoRec.ts index e38369ed..ed1e2f33 100644 --- a/app/client/models/entities/DocInfoRec.ts +++ b/app/client/models/entities/DocInfoRec.ts @@ -18,7 +18,7 @@ export function createDocInfoRec(this: DocInfoRec, docModel: DocModel): void { return tab ? tab.viewRef() : 0; })); this.newDefaultViewId = this.autoDispose(ko.pureComputed(() => { - const page = docModel.allDocPages.at(0); + const page = docModel.visibleDocPages()[0]; return page ? page.viewRef() : 0; })); } diff --git a/app/client/models/entities/PageRec.ts b/app/client/models/entities/PageRec.ts index 19707c35..2c1f3421 100644 --- a/app/client/models/entities/PageRec.ts +++ b/app/client/models/entities/PageRec.ts @@ -4,8 +4,13 @@ import * as ko from 'knockout'; // Represents a page entry in the tree of pages. export interface PageRec extends IRowModel<"_grist_Pages"> { view: ko.Computed; + isHidden: ko.Computed; } export function createPageRec(this: PageRec, docModel: DocModel): void { this.view = refRecord(docModel.views, this.viewRef); + this.isHidden = ko.pureComputed(() => { + const name = this.view().name(); + return !name || (name === 'GristDocTour' && !docModel.showDocTourTable); + }); } diff --git a/app/client/ui/Pages.ts b/app/client/ui/Pages.ts index 907b1455..c9ff0c9c 100644 --- a/app/client/ui/Pages.ts +++ b/app/client/ui/Pages.ts @@ -4,22 +4,36 @@ import { GristDoc } from "app/client/components/GristDoc"; import { PageRec } from "app/client/models/DocModel"; import { urlState } from "app/client/models/gristUrlState"; import * as MetaTableModel from "app/client/models/MetaTableModel"; -import { find as findInTree, fromTableData, TreeItemRecord } from "app/client/models/TreeModel"; +import { find as findInTree, fromTableData, TreeItemRecord, TreeRecord, + TreeTableData} from "app/client/models/TreeModel"; import { TreeViewComponent } from "app/client/ui/TreeViewComponent"; import { confirmModal } from 'app/client/ui2018/modals'; import { buildPageDom, PageActions } from "app/client/ui2018/pages"; import { mod } from 'app/common/gutil'; -import { Computed, Disposable, dom, observable, Observable } from "grainjs"; +import { Computed, Disposable, dom, fromKo, observable, Observable } from "grainjs"; // build dom for the tree view of pages export function buildPagesDom(owner: Disposable, activeDoc: GristDoc, isOpen: Observable) { const pagesTable = activeDoc.docModel.pages; const buildDom = buildDomFromTable.bind(null, pagesTable, activeDoc); + const records = Computed.create(owner, (use) => + use(activeDoc.docModel.visibleDocPages).map(page => ({ + id: page.getRowId(), + indentation: use(page.indentation), + pagePos: use(page.pagePos), + viewRef: use(page.viewRef), + })) + ); + const getTreeTableData = (): TreeTableData => ({ + getRecords: () => records.get(), + sendTableActions: (...args) => pagesTable.tableData.sendTableActions(...args), + }); + // create the model and keep in sync with the table - const model = observable(fromTableData(pagesTable.tableData, buildDom)); - owner.autoDispose(pagesTable.tableData.tableActionEmitter.addListener(() => { - model.set(fromTableData(pagesTable.tableData, buildDom, model.get())); + const model = observable(fromTableData(getTreeTableData(), buildDom)); + owner.autoDispose(records.addListener(() => { + model.set(fromTableData(getTreeTableData(), buildDom, model.get())); })); // create a computed that reads the selected page from the url and return the corresponding item @@ -69,15 +83,7 @@ function buildDomFromTable(pagesTable: MetaTableModel, activeDoc: Grist actions.isRemoveDisabled = () => (docModel.allTables.all().length <= 1); } - const maybeHiddenPageName = Computed.create(activeDoc, (use) => { - const name = use(pageName); - if (name === 'GristDocTour' && !activeDoc.showDocTourTable) { - return ''; - } - return name; - }); - - return buildPageDom(maybeHiddenPageName, actions, urlState().setLinkUrl({docPage: viewId})); + return buildPageDom(fromKo(pageName), actions, urlState().setLinkUrl({docPage: viewId})); } // Select another page in cyclic ordering of pages. Order is downard if given a positive `delta`,