From c7c79508077f671b3f12afc8ae8ca28105f460ec Mon Sep 17 00:00:00 2001 From: fflorent Date: Wed, 12 Jun 2024 10:59:26 +0200 Subject: [PATCH 01/33] first functionnal POC Co-authored-by: hexaltation Co-authored-by: fflorent --- app/client/models/DocPageModel.ts | 13 ++++--- app/client/ui/DocumentSettings.ts | 59 ++++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/app/client/models/DocPageModel.ts b/app/client/models/DocPageModel.ts index f0a2683b63..78069d8172 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 b2c9ba287b..46eaca04a7 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; From bb9f92801465248abe0091f76da6ebf068975ee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Cutzach?= Date: Tue, 20 Aug 2024 17:48:44 +0200 Subject: [PATCH 02/33] feat: update UX according figma --- app/client/ui/DocumentSettings.ts | 16 +++++++++------- app/client/ui2018/checkbox.ts | 5 +++++ app/client/ui2018/modals.ts | 3 ++- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/app/client/ui/DocumentSettings.ts b/app/client/ui/DocumentSettings.ts index 46eaca04a7..33b41a3605 100644 --- a/app/client/ui/DocumentSettings.ts +++ b/app/client/ui/DocumentSettings.ts @@ -65,7 +65,7 @@ export class DocSettingsPage extends Disposable { const isDocEditor = isOwnerOrEditor(docPageModel.currentDoc.get()); return cssContainer( - dom.create(AdminSection, t('Document Settings'), [ + dom.create(cssAdminSection, t('Document Settings'), [ dom.create(AdminSectionItem, { id: 'timezone', name: t('Time Zone'), @@ -89,7 +89,7 @@ export class DocSettingsPage extends Disposable { }), ]), - dom.create(AdminSection, t('Data Engine'), [ + dom.create(cssAdminSection, t('Data Engine'), [ dom.create(AdminSectionItem, { id: 'timings', name: t('Formula timer'), @@ -122,7 +122,6 @@ export class DocSettingsPage extends Disposable { )), disabled: isDocOwner ? false : t('Only available to document owners'), }), - dom.create(AdminSectionItem, { id: 'reload', name: t('Reload'), @@ -130,7 +129,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'), @@ -139,7 +137,7 @@ export class DocSettingsPage extends Disposable { }) : null, ]), - dom.create(AdminSection, t('API'), [ + dom.create(cssAdminSection, t('API'), [ dom.create(AdminSectionItem, { id: 'documentId', name: t('Document ID'), @@ -188,7 +186,6 @@ export class DocSettingsPage extends Disposable { href: getApiConsoleLink(docPageModel), }), }), - dom.create(AdminSectionItem, { id: 'webhooks', name: t('Webhooks'), @@ -196,7 +193,8 @@ export class DocSettingsPage extends Disposable { value: cssSmallLinkButton(t('Manage webhooks'), urlState().setLinkUrl({docPage: 'webhook'})), }), ]), - dom.create(AdminSection, t('Document conversion'), [ + + dom.create(cssAdminSection, t('Document conversion'), [ dom.create(AdminSectionItem, { id: 'document-type', name: t('Document type'), @@ -566,3 +564,7 @@ const cssWrap = styled('p', ` const cssRedText = styled('span', ` color: ${theme.errorText}; `); + +const cssAdminSection = styled(AdminSection, ` + max-width: 750px; +`); diff --git a/app/client/ui2018/checkbox.ts b/app/client/ui2018/checkbox.ts index 239989bfbf..4191bfaec0 100644 --- a/app/client/ui2018/checkbox.ts +++ b/app/client/ui2018/checkbox.ts @@ -25,6 +25,9 @@ export const cssLabel = styled('label', ` margin-bottom: 0px; flex-shrink: 0; + align-items: center; + justify-content: center; + outline: none; user-select: none; @@ -226,6 +229,8 @@ const cssBlockCheckbox = styled('div', ` } &-block { pointer-events: none; + border-color: ${theme.controlFg}; + border-width: 2px; } &-block a { pointer-events: all; diff --git a/app/client/ui2018/modals.ts b/app/client/ui2018/modals.ts index 77b2d1ad32..5858cc6c8c 100644 --- a/app/client/ui2018/modals.ts +++ b/app/client/ui2018/modals.ts @@ -623,10 +623,11 @@ export const cssModalBody = styled('div', ` export const cssModalButtons = styled('div', ` margin: 40px 0 0 0; + text-align: right; & > button, & > .${cssButton.className} { - margin: 0 8px 0 0; + margin: 0 0 0 8px; } `); From 57f6b3baae2a2706cc9d0c9e8ce054aad1eed9e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Cutzach?= Date: Thu, 22 Aug 2024 17:05:17 +0200 Subject: [PATCH 03/33] wip: working implementation todo: tests, notification of success --- app/client/ui/AdminPanelCss.ts | 1 + app/client/ui/DocumentSettings.ts | 188 +++++++++++++++++++++++------- app/client/ui2018/checkbox.ts | 1 - 3 files changed, 147 insertions(+), 43 deletions(-) diff --git a/app/client/ui/AdminPanelCss.ts b/app/client/ui/AdminPanelCss.ts index 2853a3fa36..d0d48e8bc8 100644 --- a/app/client/ui/AdminPanelCss.ts +++ b/app/client/ui/AdminPanelCss.ts @@ -159,6 +159,7 @@ const cssItemName = styled('div', ` `); const cssItemDescription = styled('div', ` + max-width: 320px; margin-right: auto; margin-bottom: -1px; /* aligns with the value */ `); diff --git a/app/client/ui/DocumentSettings.ts b/app/client/ui/DocumentSettings.ts index 33b41a3605..3a2e8f067f 100644 --- a/app/client/ui/DocumentSettings.ts +++ b/app/client/ui/DocumentSettings.ts @@ -42,7 +42,6 @@ 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')) )) @@ -87,6 +86,18 @@ export class DocSettingsPage extends Disposable { {defaultCurrencyLabel: t("Local currency ({{currency}})", {currency: getCurrency(l)})}) ) }), + dom.create(AdminSectionItem, { + id: 'templateMode', + name: t('Template mode'), + description: t('Special document mode'), + value: cssDocTypeContainer( + dom.create(displayCurrentType, docPageModel.type, /*docPageModel.currentDocId.get()*/), + cssSmallButton(t('Edit'), + dom.on('click', this._doSetDocumentType.bind(this, true)) + ) + ), + disabled: isDocEditor ? false : t('Only available to document editors'), + }), ]), dom.create(cssAdminSection, t('Data Engine'), [ @@ -193,15 +204,6 @@ export class DocSettingsPage extends Disposable { value: cssSmallLinkButton(t('Manage webhooks'), urlState().setLinkUrl({docPage: 'webhook'})), }), ]), - - dom.create(cssAdminSection, 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()), - }), - ]), ); } @@ -232,11 +234,11 @@ export class DocSettingsPage extends Disposable { const docPageModel = this._gristDoc.docPageModel; modal((ctl, owner) => { this.onDispose(() => ctl.close()); - const selected = Observable.create