diff --git a/app/client/components/GristDoc.ts b/app/client/components/GristDoc.ts index 08e41337..0b85cf5d 100644 --- a/app/client/components/GristDoc.ts +++ b/app/client/components/GristDoc.ts @@ -59,6 +59,7 @@ 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'); @@ -94,6 +95,7 @@ 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; @@ -158,7 +160,17 @@ export class GristDoc extends DisposableWithEvents { // Grainjs observable for current view id, which may be a string such as 'code'. this.activeViewId = Computed.create(this, (use) => { - return use(urlState().state).docPage || use(defaultViewId); + let result = use(urlState().state).docPage; + 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 + } + } + return result || use(defaultViewId); }); // This viewModel reflects the currently active view, relying on the fact that @@ -335,7 +347,7 @@ export class GristDoc extends DisposableWithEvents { dom.domComputed(this.activeViewId, (viewId) => ( viewId === 'code' ? dom.create((owner) => owner.autoDispose(CodeEditorPanel.create(this))) : viewId === 'acl' ? dom.create((owner) => owner.autoDispose(AccessRules.create(this, this))) : - viewId === 'new' ? null : + viewId === 'new' || viewId == 'GristDocTour' ? null : dom.create((owner) => (this._viewLayout = ViewLayout.create(owner, this, viewId))) )), ); diff --git a/app/client/ui/Pages.ts b/app/client/ui/Pages.ts index ec6dd43e..907b1455 100644 --- a/app/client/ui/Pages.ts +++ b/app/client/ui/Pages.ts @@ -69,7 +69,15 @@ function buildDomFromTable(pagesTable: MetaTableModel, activeDoc: Grist actions.isRemoveDisabled = () => (docModel.allTables.all().length <= 1); } - return buildPageDom(pageName, actions, urlState().setLinkUrl({docPage: viewId})); + 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})); } // Select another page in cyclic ordering of pages. Order is downard if given a positive `delta`, diff --git a/app/client/ui2018/pages.ts b/app/client/ui2018/pages.ts index dbca3e6d..d43e7766 100644 --- a/app/client/ui2018/pages.ts +++ b/app/client/ui2018/pages.ts @@ -5,7 +5,6 @@ import { colors } from "app/client/ui2018/cssVars"; import { icon } from "app/client/ui2018/icons"; import { menu, menuItem, menuText } from "app/client/ui2018/menus"; import { dom, domComputed, DomElementArg, makeTestId, observable, Observable, styled } from "grainjs"; -import * as ko from "knockout"; const testId = makeTestId('test-docpage-'); @@ -18,15 +17,12 @@ export interface PageActions { isReadonly: Observable; } -// to work with existing code we need to support both knockout and grainjs observable -type NameType = Observable|ko.Observable; - // build the dom for a document page entry. It shows an icon (for now the first letter of the name, // but later we'll support user selected icon), the name and a dots menu containing a "Rename" and // "Remove" entries. Clicking "Rename" turns the page name into an editable input, which then call // the actions.onRename callback with the new name. Setting actions.onRemove to undefined disables // the item in the menu. -export function buildPageDom(name: NameType, actions: PageActions, ...args: DomElementArg[]) { +export function buildPageDom(name: Observable, actions: PageActions, ...args: DomElementArg[]) { const isRenaming = observable(false); const pageMenu = () => [ @@ -59,7 +55,7 @@ export function buildPageDom(name: NameType, actions: PageActions, ...args: DomE cssPageInitial(dom.text((use) => use(name)[0])), cssEditorInput( { - initialValue: typeof name === 'function' ? name() : name.get() || '', + initialValue: name.get() || '', save: (val) => actions.onRename(val), close: () => isRenaming.set(false) }, diff --git a/app/common/gristUrls.ts b/app/common/gristUrls.ts index 05ae49e8..ea3f6dac 100644 --- a/app/common/gristUrls.ts +++ b/app/common/gristUrls.ts @@ -7,7 +7,7 @@ import {Document} from 'app/common/UserAPI'; import clone = require('lodash/clone'); import pickBy = require('lodash/pickBy'); -export type IDocPage = number | 'new' | 'code' | 'acl'; +export type IDocPage = number | 'new' | 'code' | 'acl' | 'GristDocTour'; // What page to show in the user's home area. Defaults to 'workspace' if a workspace is set, and // to 'all' otherwise. @@ -329,8 +329,8 @@ export function userOverrideParams(email: string|null, extraState?: IGristUrlSta * parseDocPage is a noop if p is 'new' or 'code', otherwise parse to integer */ function parseDocPage(p: string) { - if (['new', 'code', 'acl'].includes(p)) { - return p as 'new'|'code'|'acl'; + if (['new', 'code', 'acl', 'GristDocTour'].includes(p)) { + return p as 'new'|'code'|'acl'|'GristDocTour'; } return parseInt(p, 10); }