diff --git a/app/client/models/DocPageModel.ts b/app/client/models/DocPageModel.ts index f0a2683b63..7130830f2c 100644 --- a/app/client/models/DocPageModel.ts +++ b/app/client/models/DocPageModel.ts @@ -24,7 +24,16 @@ import {buildUrlId, IGristUrlState, parseUrlId, UrlIdParts} from 'app/common/gri import {getReconnectTimeout} from 'app/common/gutil'; import {canEdit, isOwner} from 'app/common/roles'; import {UserInfo} from 'app/common/User'; -import {Document, NEW_DOCUMENT_CODE, Organization, UserAPI, Workspace} from 'app/common/UserAPI'; +import { + DOCTYPE_TEMPLATE, + DOCTYPE_TUTORIAL, + Document, + DocumentType, + NEW_DOCUMENT_CODE, + Organization, + UserAPI, + Workspace +} from 'app/common/UserAPI'; import {Holder, Observable, subscribe} from 'grainjs'; import {Computed, Disposable, dom, DomArg, DomElementArg} from 'grainjs'; import {makeT} from 'app/client/lib/localization'; @@ -87,7 +96,7 @@ export interface DocPageModel { isTutorialTrunk: Observable; isTutorialFork: Observable; isTemplate: Observable; - + type: Observable; importSources: ImportSource[]; undoState: Observable; // See UndoStack for details. @@ -147,6 +156,8 @@ export class DocPageModelImpl extends Disposable implements DocPageModel { (use, doc) => doc ? doc.isTutorialFork : false); public readonly isTemplate = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isTemplate : false); + public readonly type = Computed.create(this, this.currentDoc, + (use, doc) => doc?.type ?? null); public readonly importSources: ImportSource[] = []; @@ -499,7 +510,8 @@ function buildDocInfo(doc: Document, mode: OpenDocMode | undefined): DocInfo { const isFork = Boolean(idParts.forkId || idParts.snapshotId); const isBareFork = isFork && idParts.trunkId === NEW_DOCUMENT_CODE; const isSnapshot = Boolean(idParts.snapshotId); - const isTutorial = doc.type === 'tutorial'; + const type = doc.type; + const isTutorial = type === DOCTYPE_TUTORIAL; const isTutorialTrunk = isTutorial && !isFork && mode !== 'default'; const isTutorialFork = isTutorial && isFork; @@ -511,7 +523,7 @@ function buildDocInfo(doc: Document, mode: OpenDocMode | undefined): DocInfo { // mode. Since the document's 'openMode' has no effect, don't bother trying // to set it here, as it'll potentially be confusing for other code reading it. openMode = 'default'; - } else if (!isFork && doc.type === 'template') { + } else if (!isFork && type === DOCTYPE_TEMPLATE) { // Templates should always open in fork mode by default. openMode = 'fork'; } else { @@ -521,7 +533,7 @@ function buildDocInfo(doc: Document, mode: OpenDocMode | undefined): DocInfo { } const isPreFork = openMode === 'fork'; - const isTemplate = doc.type === 'template' && (isFork || isPreFork); + const isTemplate = type === DOCTYPE_TEMPLATE && (isFork || isPreFork); const isEditable = !isSnapshot && (canEdit(doc.access) || isPreFork); return { ...doc, @@ -534,6 +546,7 @@ function buildDocInfo(doc: Document, mode: OpenDocMode | undefined): DocInfo { isSnapshot, isTutorialTrunk, isTutorialFork, + type, isTemplate, isReadonly: !isEditable, idParts, diff --git a/app/client/ui/DocumentSettings.ts b/app/client/ui/DocumentSettings.ts index b2c9ba287b..901dd4cccc 100644 --- a/app/client/ui/DocumentSettings.ts +++ b/app/client/ui/DocumentSettings.ts @@ -29,7 +29,18 @@ import {commonUrls, GristLoadConfig} from 'app/common/gristUrls'; import {not, propertyCompare} from 'app/common/gutil'; import {getCurrency, locales} from 'app/common/Locales'; import {isOwner, isOwnerOrEditor} from 'app/common/roles'; -import {Computed, Disposable, dom, fromKo, IDisposableOwner, makeTestId, Observable, styled} from 'grainjs'; +import {DOCTYPE_NORMAL, DOCTYPE_TEMPLATE, DOCTYPE_TUTORIAL, DocumentType} from 'app/common/UserAPI'; +import { + Computed, + Disposable, + dom, + DomElementMethod, + fromKo, + IDisposableOwner, + makeTestId, + Observable, + styled +} from 'grainjs'; import * as moment from 'moment-timezone'; const t = makeT('DocumentSettings'); @@ -85,6 +96,22 @@ export class DocSettingsPage extends Disposable { {defaultCurrencyLabel: t("Local currency ({{currency}})", {currency: getCurrency(l)})}) ) }), + dom.create(AdminSectionItem, { + id: 'templateMode', + name: t('Template mode'), + description: t('Change document type'), + value: cssDocTypeContainer( + dom.create( + displayCurrentType, + docPageModel.type, + ), + cssSmallButton(t('Edit'), + dom.on('click', this._buildDocumentTypeModal.bind(this)), + testId('doctype-edit') + ), + ), + disabled: isDocOwner ? false : t('Only available to document owners'), + }), ]), dom.create(AdminSection, t('Data Engine'), [ @@ -120,7 +147,6 @@ export class DocSettingsPage extends Disposable { )), disabled: isDocOwner ? false : t('Only available to document owners'), }), - dom.create(AdminSectionItem, { id: 'reload', name: t('Reload'), @@ -128,7 +154,6 @@ export class DocSettingsPage extends Disposable { value: cssSmallButton(t('Reload data engine'), dom.on('click', this._reloadEngine.bind(this, true))), disabled: isDocEditor ? false : t('Only available to document editors'), }), - canChangeEngine ? dom.create(AdminSectionItem, { id: 'python', name: t('Python'), @@ -186,7 +211,6 @@ export class DocSettingsPage extends Disposable { href: getApiConsoleLink(docPageModel), }), }), - dom.create(AdminSectionItem, { id: 'webhooks', name: t('Webhooks'), @@ -224,11 +248,11 @@ export class DocSettingsPage extends Disposable { const docPageModel = this._gristDoc.docPageModel; modal((ctl, owner) => { this.onDispose(() => ctl.close()); - const selected = Observable.create