diff --git a/app/client/models/DocPageModel.ts b/app/client/models/DocPageModel.ts index f0a2683b..78069d81 100644 --- a/app/client/models/DocPageModel.ts +++ b/app/client/models/DocPageModel.ts @@ -29,6 +29,7 @@ import {Holder, Observable, subscribe} from 'grainjs'; import {Computed, Disposable, dom, DomArg, DomElementArg} from 'grainjs'; import {makeT} from 'app/client/lib/localization'; import {logTelemetryEvent} from 'app/client/lib/telemetry'; +import {DocumentType} from 'app/common/UserAPI'; // tslint:disable:no-console @@ -87,7 +88,7 @@ export interface DocPageModel { isTutorialTrunk: Observable; isTutorialFork: Observable; isTemplate: Observable; - + type: Observable; importSources: ImportSource[]; undoState: Observable; // See UndoStack for details. @@ -147,6 +148,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 +502,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 === 'tutorial'; const isTutorialTrunk = isTutorial && !isFork && mode !== 'default'; const isTutorialFork = isTutorial && isFork; @@ -511,7 +515,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 === 'template') { // Templates should always open in fork mode by default. openMode = 'fork'; } else { @@ -521,7 +525,7 @@ function buildDocInfo(doc: Document, mode: OpenDocMode | undefined): DocInfo { } const isPreFork = openMode === 'fork'; - const isTemplate = doc.type === 'template' && (isFork || isPreFork); + const isTemplate = type === 'template' && (isFork || isPreFork); const isEditable = !isSnapshot && (canEdit(doc.access) || isPreFork); return { ...doc, @@ -534,6 +538,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 b2c9ba28..46eaca04 100644 --- a/app/client/ui/DocumentSettings.ts +++ b/app/client/ui/DocumentSettings.ts @@ -31,6 +31,7 @@ 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 * as moment from 'moment-timezone'; +import {DocumentType} from 'app/common/UserAPI'; const t = makeT('DocumentSettings'); const testId = makeTestId('test-settings-'); @@ -41,6 +42,7 @@ export class DocSettingsPage extends Disposable { private _timezone = this._docInfo.timezone; private _locale: KoSaveableObservable = this._docInfo.documentSettingsJson.prop('locale'); private _currency: KoSaveableObservable = this._docInfo.documentSettingsJson.prop('currency'); + // private _type: KoSaveableObservable = this._docInfo.documentSettingsJson.prop('type'); private _engine: Computed = Computed.create(this, ( use => use(this._docInfo.documentSettingsJson.prop('engine')) )) @@ -194,6 +196,14 @@ export class DocSettingsPage extends Disposable { value: cssSmallLinkButton(t('Manage webhooks'), urlState().setLinkUrl({docPage: 'webhook'})), }), ]), + dom.create(AdminSection, t('Document conversion'), [ + dom.create(AdminSectionItem, { + id: 'document-type', + name: t('Document type'), + description: t('Convert the document'), + value: dom.create(buildTypeSelect, docPageModel.type, docPageModel.currentDocId.get()), + }), + ]), ); } @@ -298,7 +308,15 @@ export class DocSettingsPage extends Disposable { } } - +function persistType(type: string|null, docId: string|undefined){ + docId = docId?.split("~")[0]; + return fetch(`/o/docs/api/docs/${docId}`, + { method:'PATCH', + headers: {"Content-Type": "application/json"}, + credentials: 'include', + body:JSON.stringify({type}) + }).catch((err)=>{ console.log(err); }); +} function getApiConsoleLink(docPageModel: DocPageModel) { const url = new URL(location.href); @@ -343,6 +361,45 @@ function buildLocaleSelect( ); } +type DocumentTypeItem = ACSelectItem & {type?: string}; + +function buildTypeSelect( + owner: IDisposableOwner, + type: Observable, + id: string|undefined, +) { + const typeList: DocumentTypeItem[] = [{ + label: t('Regular'), + type: '' + }, { + label: t('Template'), + type: 'template' + }, + { + label: t('Tutorial'), + type: 'tutorial' + }].map((el) => ({ + ...el, + value: el.label, + cleanText: el.label.trim().toLowerCase() + })); + const typeObs = Computed.create(owner, use => { + const typeCode = use(type)??""; + const typeName = typeList.find(ty => ty.type === typeCode)?.label || typeCode; + return typeName; + }); + const acIndex = new ACIndexImpl(typeList, {maxResults: 200, keepOrder: true}); + return buildACSelect(owner, { + acIndex, valueObs: typeObs, + save(_value, item: DocumentTypeItem | undefined) { + if (!item) { throw new Error("Invalid DocumentType"); } + persistType(item.type!, id) + .then(()=>window.location.reload()) + .catch(err=>console.log(err)); + } + }); +} + const cssContainer = styled('div', ` overflow-y: auto; position: relative;