From e3509660024be64bd35893dad79f05a31b90010d Mon Sep 17 00:00:00 2001 From: Seven <76855829+seven-of-eleven@users.noreply.github.com> Date: Sat, 22 Feb 2025 16:18:02 -0500 Subject: [PATCH 01/13] Keyboard shortcuts popup window fixed to disappear. You can use command + / or click outside the popup window for it to hide again. --- app/client/ui/App.ts | 247 ++++++++++++++++++++++------------- app/plugin/DocApiTypes-ti.ts | 5 + 2 files changed, 161 insertions(+), 91 deletions(-) diff --git a/app/client/ui/App.ts b/app/client/ui/App.ts index 2b2b1484bc..4197d0a224 100644 --- a/app/client/ui/App.ts +++ b/app/client/ui/App.ts @@ -1,34 +1,38 @@ -import {ClientScope} from 'app/client/components/ClientScope'; -import * as Clipboard from 'app/client/components/Clipboard'; -import {Comm} from 'app/client/components/Comm'; -import * as commandList from 'app/client/components/commandList'; -import * as commands from 'app/client/components/commands'; -import {unsavedChanges} from 'app/client/components/UnsavedChanges'; -import {get as getBrowserGlobals} from 'app/client/lib/browserGlobals'; -import {isDesktop} from 'app/client/lib/browserInfo'; -import {FocusLayer} from 'app/client/lib/FocusLayer'; -import * as koUtil from 'app/client/lib/koUtil'; -import {reportError, TopAppModel, TopAppModelImpl} from 'app/client/models/AppModel'; -import {DocPageModel} from 'app/client/models/DocPageModel'; -import {setUpErrorHandling} from 'app/client/models/errors'; -import {createAppUI} from 'app/client/ui/AppUI'; -import {addViewportTag} from 'app/client/ui/viewport'; -import {attachCssRootVars} from 'app/client/ui2018/cssVars'; -import {attachTheme} from 'app/client/ui2018/theme'; -import {BaseAPI} from 'app/common/BaseAPI'; -import {CommDocError} from 'app/common/CommTypes'; -import {DisposableWithEvents} from 'app/common/DisposableWithEvents'; -import {fetchFromHome} from 'app/common/urlUtils'; -import {ISupportedFeatures} from 'app/common/UserConfig'; -import {dom} from 'grainjs'; -import * as ko from 'knockout'; -import {makeT} from 'app/client/lib/localization'; - -const t = makeT('App'); +import { ClientScope } from "app/client/components/ClientScope"; +import * as Clipboard from "app/client/components/Clipboard"; +import { Comm } from "app/client/components/Comm"; +import * as commandList from "app/client/components/commandList"; +import * as commands from "app/client/components/commands"; +import { unsavedChanges } from "app/client/components/UnsavedChanges"; +import { get as getBrowserGlobals } from "app/client/lib/browserGlobals"; +import { isDesktop } from "app/client/lib/browserInfo"; +import { FocusLayer } from "app/client/lib/FocusLayer"; +import * as koUtil from "app/client/lib/koUtil"; +import { + reportError, + TopAppModel, + TopAppModelImpl, +} from "app/client/models/AppModel"; +import { DocPageModel } from "app/client/models/DocPageModel"; +import { setUpErrorHandling } from "app/client/models/errors"; +import { createAppUI } from "app/client/ui/AppUI"; +import { addViewportTag } from "app/client/ui/viewport"; +import { attachCssRootVars } from "app/client/ui2018/cssVars"; +import { attachTheme } from "app/client/ui2018/theme"; +import { BaseAPI } from "app/common/BaseAPI"; +import { CommDocError } from "app/common/CommTypes"; +import { DisposableWithEvents } from "app/common/DisposableWithEvents"; +import { fetchFromHome } from "app/common/urlUtils"; +import { ISupportedFeatures } from "app/common/UserConfig"; +import { dom } from "grainjs"; +import * as ko from "knockout"; +import { makeT } from "app/client/lib/localization"; + +const t = makeT("App"); // tslint:disable:no-console -const G = getBrowserGlobals('document', 'window'); +const G = getBrowserGlobals("document", "window"); /** * Main Grist App UI component. @@ -40,13 +44,13 @@ export class App extends DisposableWithEvents { public comm = this.autoDispose(Comm.create(this._checkError.bind(this))); public clientScope: ClientScope; public features: ko.Computed; - public topAppModel: TopAppModel; // Exposed because used by test/nbrowser/gristUtils. + public topAppModel: TopAppModel; // Exposed because used by test/nbrowser/gristUtils. - private _settings: ko.Observable<{features?: ISupportedFeatures}>; + private _settings: ko.Observable<{ features?: ISupportedFeatures }>; // Track the version of the server we are communicating with, so that if it changes // we can choose to refresh the client also. - private _serverVersion: string|null = null; + private _serverVersion: string | null = null; // Track the most recently created DocPageModel, for some error handling. private _mostRecentDocPageModel?: DocPageModel; @@ -72,12 +76,12 @@ export class App extends DisposableWithEvents { // scrolling and showing of mobile keyboard). But we still rely on 'clipboard_focus' and // 'clipboard_blur' events to know when the "app" has a focus (rather than a particular // input), by making document.body focusable and using a FocusLayer with it as the default. - document.body.setAttribute('tabindex', '-1'); + document.body.setAttribute("tabindex", "-1"); FocusLayer.create(this, { defaultFocusElem: document.body, allowFocus: Clipboard.allowFocus, - onDefaultFocus: () => this.trigger('clipboard_focus'), - onDefaultBlur: () => this.trigger('clipboard_blur'), + onDefaultFocus: () => this.trigger("clipboard_focus"), + onDefaultBlur: () => this.trigger("clipboard_blur"), }); } @@ -85,62 +89,117 @@ export class App extends DisposableWithEvents { const isHelpPaneVisible = ko.observable(false); - G.document.querySelector('#grist-logo-wrapper')?.remove(); - - // Help pop-up pane const helpDiv = document.body.appendChild( - dom('div.g-help', - dom.show(isHelpPaneVisible), - dom('table.g-help-table', - dom('thead', - dom('tr', - dom('th', t("Key")), - dom('th', t("Description")) - ) + dom( + "div.g-help", + dom.show(isHelpPaneVisible), // Toggle visibility dynamically + dom( + "table.g-help-table", + dom( + "thead", + dom("tr", dom("th", t("Key")), dom("th", t("Description"))), ), dom.forEach(commandList.groups, (group) => { - const cmds = group.commands.filter((cmd) => Boolean(cmd.desc && cmd.keys.length && !cmd.deprecated)); - return cmds.length > 0 ? - dom('tbody', - dom('tr', - dom('td', {colspan: '2'}, group.group) - ), - dom.forEach(cmds, (cmd) => - dom('tr', - dom('td', commands.allCommands[cmd.name]!.getKeysDom()), - dom('td', cmd.desc) - ) + const cmds = group.commands.filter((cmd) => + Boolean(cmd.desc && cmd.keys.length && !cmd.deprecated), + ); + return cmds.length > 0 + ? dom( + "tbody", + dom("tr", dom("td", { colspan: "2" }, group.group)), + dom.forEach(cmds, (cmd) => + dom( + "tr", + dom("td", commands.allCommands[cmd.name]!.getKeysDom()), + dom("td", cmd.desc), + ), + ), ) - ) : null; - }) - ) - ) + : null; + }), + ), + ), + ); + this.onDispose(() => { + dom.domDispose(helpDiv); + helpDiv.remove(); + }); + + /** Click outside the popup to close it */ + document.addEventListener("click", function (event) { + if (isHelpPaneVisible() && !helpDiv.contains(event.target as Node)) { + isHelpPaneVisible(false); // Hide the help menu + } + }); + + /** Use "Cmd + /" to toggle */ + this.autoDispose( + commands.createGroup( + { + help() { + G.window.open("help", "_blank").focus(); + }, + shortcuts() { + isHelpPaneVisible(!isHelpPaneVisible()); + }, // FIXED: Toggle Open/Close + historyBack() { + G.window.history.back(); + }, + historyForward() { + G.window.history.forward(); + }, + }, + this, + true, + ), ); - this.onDispose(() => { dom.domDispose(helpDiv); helpDiv.remove(); }); - - this.autoDispose(commands.createGroup({ - help() { G.window.open('help', '_blank').focus(); }, - shortcuts() { isHelpPaneVisible(true); }, - historyBack() { G.window.history.back(); }, - historyForward() { G.window.history.forward(); }, - }, this, true)); - - this.autoDispose(commands.createGroup({ - cancel() { isHelpPaneVisible(false); }, - cursorDown() { helpDiv.scrollBy(0, 30); }, // 30 is height of the row in the help screen - cursorUp() { helpDiv.scrollBy(0, -30); }, - pageUp() { helpDiv.scrollBy(0, -helpDiv.clientHeight); }, - pageDown() { helpDiv.scrollBy(0, helpDiv.clientHeight); }, - moveToFirstField() { helpDiv.scrollTo(0, 0); }, // home - moveToLastField() { helpDiv.scrollTo(0, helpDiv.scrollHeight); }, // end - find() { return true; }, // restore browser search - help() { isHelpPaneVisible(false); }, - }, this, isHelpPaneVisible)); - - this.listenTo(this.comm, 'clientConnect', (message) => { - console.log(`App clientConnect event: needReload ${message.needReload} version ${message.serverVersion}`); + + /** Ensure menu closes on cancel */ + this.autoDispose( + commands.createGroup( + { + cancel() { + isHelpPaneVisible(false); + }, // Close menu when Esc/Cancel is triggered + cursorDown() { + helpDiv.scrollBy(0, 30); + }, + cursorUp() { + helpDiv.scrollBy(0, -30); + }, + pageUp() { + helpDiv.scrollBy(0, -helpDiv.clientHeight); + }, + pageDown() { + helpDiv.scrollBy(0, helpDiv.clientHeight); + }, + moveToFirstField() { + helpDiv.scrollTo(0, 0); + }, // home + moveToLastField() { + helpDiv.scrollTo(0, helpDiv.scrollHeight); + }, // end + find() { + return true; + }, // restore browser search + help() { + isHelpPaneVisible(false); + }, // Close menu + }, + this, + isHelpPaneVisible, + ), + ); + + this.listenTo(this.comm, "clientConnect", (message) => { + console.log( + `App clientConnect event: needReload ${message.needReload} version ${message.serverVersion}`, + ); this._settings(message.settings); - if (message.serverVersion === 'dead' || (this._serverVersion && this._serverVersion !== message.serverVersion)) { + if ( + message.serverVersion === "dead" || + (this._serverVersion && this._serverVersion !== message.serverVersion) + ) { console.log("Upgrading..."); // Server has upgraded. Upgrade client. TODO: be gentle and polite. return this.reload(); @@ -153,24 +212,24 @@ export class App extends DisposableWithEvents { } }); - this.listenTo(this.comm, 'connectState', (isConnected: boolean) => { + this.listenTo(this.comm, "connectState", (isConnected: boolean) => { this.topAppModel.notifier.setConnectState(isConnected); }); - this.listenTo(this.comm, 'docShutdown', () => { + this.listenTo(this.comm, "docShutdown", () => { console.log("Received docShutdown"); // Reload on next tick, to let other objects process 'docShutdown' before they get disposed. setTimeout(() => this.reloadPane(), 0); }); - this.listenTo(this.comm, 'docError', (msg: CommDocError) => { + this.listenTo(this.comm, "docError", (msg: CommDocError) => { this._checkError(new Error(msg.data.message)); }); // When the document is unloaded, dispose the app, allowing it to do any needed // cleanup (e.g. Document on disposal triggers closeDoc message to the server). It needs to be // in 'beforeunload' rather than 'unload', since websocket is closed by the time of 'unload'. - G.window.addEventListener('beforeunload', (ev: BeforeUnloadEvent) => { + G.window.addEventListener("beforeunload", (ev: BeforeUnloadEvent) => { if (unsavedChanges.haveUnsavedChanges()) { // Following https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event ev.returnValue = true; @@ -192,7 +251,9 @@ export class App extends DisposableWithEvents { // We want to test errors from Selenium, but errors we can trigger using driver.executeScript() // will be impossible for the application to report properly (they seem to be considered not of // "same-origin"). So this silly callback is for tests to generate a fake error. - public testTriggerError(msg: string) { throw new Error(msg); } + public testTriggerError(msg: string) { + throw new Error(msg); + } public reloadPane() { console.log("reloadPane"); @@ -230,12 +291,16 @@ export class App extends DisposableWithEvents { * is available in weblate and good translations have been updated. */ public checkSpecialTranslationKey() { - return t('Translators: please translate this only when your language is ready to be offered to users'); + return t( + "Translators: please translate this only when your language is ready to be offered to users", + ); } // Get the user profile for testing purposes public async testGetProfile(): Promise { - const resp = await fetchFromHome('/api/profile/user', {credentials: 'include'}); + const resp = await fetchFromHome("/api/profile/user", { + credentials: "include", + }); return resp.json(); } diff --git a/app/plugin/DocApiTypes-ti.ts b/app/plugin/DocApiTypes-ti.ts index 4954a573c0..2dc50c5ffc 100644 --- a/app/plugin/DocApiTypes-ti.ts +++ b/app/plugin/DocApiTypes-ti.ts @@ -96,6 +96,10 @@ export const SetAttachmentStorePost = t.iface([], { export const AttachmentStore = t.union(t.lit('internal'), t.lit('external')); +export const AttachmentStoreDesc = t.iface([], { + "label": "string", +}); + const exportedTypeSuite: t.ITypeSuite = { NewRecord, NewRecordWithStringId, @@ -116,5 +120,6 @@ const exportedTypeSuite: t.ITypeSuite = { SqlPost, SetAttachmentStorePost, AttachmentStore, + AttachmentStoreDesc, }; export default exportedTypeSuite; From 56a73d04c19e10d028a2621a0c772be000691d20 Mon Sep 17 00:00:00 2001 From: seven-of-eleven Date: Mon, 24 Feb 2025 10:35:29 -0500 Subject: [PATCH 02/13] Updated formatting of the file. --- app/client/ui/App.ts | 229 ++++++++++++++++--------------------------- 1 file changed, 85 insertions(+), 144 deletions(-) diff --git a/app/client/ui/App.ts b/app/client/ui/App.ts index 4197d0a224..689cd27232 100644 --- a/app/client/ui/App.ts +++ b/app/client/ui/App.ts @@ -1,38 +1,34 @@ -import { ClientScope } from "app/client/components/ClientScope"; -import * as Clipboard from "app/client/components/Clipboard"; -import { Comm } from "app/client/components/Comm"; -import * as commandList from "app/client/components/commandList"; -import * as commands from "app/client/components/commands"; -import { unsavedChanges } from "app/client/components/UnsavedChanges"; -import { get as getBrowserGlobals } from "app/client/lib/browserGlobals"; -import { isDesktop } from "app/client/lib/browserInfo"; -import { FocusLayer } from "app/client/lib/FocusLayer"; -import * as koUtil from "app/client/lib/koUtil"; -import { - reportError, - TopAppModel, - TopAppModelImpl, -} from "app/client/models/AppModel"; -import { DocPageModel } from "app/client/models/DocPageModel"; -import { setUpErrorHandling } from "app/client/models/errors"; -import { createAppUI } from "app/client/ui/AppUI"; -import { addViewportTag } from "app/client/ui/viewport"; -import { attachCssRootVars } from "app/client/ui2018/cssVars"; -import { attachTheme } from "app/client/ui2018/theme"; -import { BaseAPI } from "app/common/BaseAPI"; -import { CommDocError } from "app/common/CommTypes"; -import { DisposableWithEvents } from "app/common/DisposableWithEvents"; -import { fetchFromHome } from "app/common/urlUtils"; -import { ISupportedFeatures } from "app/common/UserConfig"; -import { dom } from "grainjs"; -import * as ko from "knockout"; -import { makeT } from "app/client/lib/localization"; - -const t = makeT("App"); +import {ClientScope} from 'app/client/components/ClientScope'; +import * as Clipboard from 'app/client/components/Clipboard'; +import {Comm} from 'app/client/components/Comm'; +import * as commandList from 'app/client/components/commandList'; +import * as commands from 'app/client/components/commands'; +import {unsavedChanges} from 'app/client/components/UnsavedChanges'; +import {get as getBrowserGlobals} from 'app/client/lib/browserGlobals'; +import {isDesktop} from 'app/client/lib/browserInfo'; +import {FocusLayer} from 'app/client/lib/FocusLayer'; +import * as koUtil from 'app/client/lib/koUtil'; +import {reportError, TopAppModel, TopAppModelImpl} from 'app/client/models/AppModel'; +import {DocPageModel} from 'app/client/models/DocPageModel'; +import {setUpErrorHandling} from 'app/client/models/errors'; +import {createAppUI} from 'app/client/ui/AppUI'; +import {addViewportTag} from 'app/client/ui/viewport'; +import {attachCssRootVars} from 'app/client/ui2018/cssVars'; +import {attachTheme} from 'app/client/ui2018/theme'; +import {BaseAPI} from 'app/common/BaseAPI'; +import {CommDocError} from 'app/common/CommTypes'; +import {DisposableWithEvents} from 'app/common/DisposableWithEvents'; +import {fetchFromHome} from 'app/common/urlUtils'; +import {ISupportedFeatures} from 'app/common/UserConfig'; +import {dom} from 'grainjs'; +import * as ko from 'knockout'; +import {makeT} from 'app/client/lib/localization'; + +const t = makeT('App'); // tslint:disable:no-console -const G = getBrowserGlobals("document", "window"); +const G = getBrowserGlobals('document', 'window'); /** * Main Grist App UI component. @@ -44,13 +40,13 @@ export class App extends DisposableWithEvents { public comm = this.autoDispose(Comm.create(this._checkError.bind(this))); public clientScope: ClientScope; public features: ko.Computed; - public topAppModel: TopAppModel; // Exposed because used by test/nbrowser/gristUtils. + public topAppModel: TopAppModel; // Exposed because used by test/nbrowser/gristUtils. - private _settings: ko.Observable<{ features?: ISupportedFeatures }>; + private _settings: ko.Observable<{features?: ISupportedFeatures}>; // Track the version of the server we are communicating with, so that if it changes // we can choose to refresh the client also. - private _serverVersion: string | null = null; + private _serverVersion: string|null = null; // Track the most recently created DocPageModel, for some error handling. private _mostRecentDocPageModel?: DocPageModel; @@ -76,12 +72,12 @@ export class App extends DisposableWithEvents { // scrolling and showing of mobile keyboard). But we still rely on 'clipboard_focus' and // 'clipboard_blur' events to know when the "app" has a focus (rather than a particular // input), by making document.body focusable and using a FocusLayer with it as the default. - document.body.setAttribute("tabindex", "-1"); + document.body.setAttribute('tabindex', '-1'); FocusLayer.create(this, { defaultFocusElem: document.body, allowFocus: Clipboard.allowFocus, - onDefaultFocus: () => this.trigger("clipboard_focus"), - onDefaultBlur: () => this.trigger("clipboard_blur"), + onDefaultFocus: () => this.trigger('clipboard_focus'), + onDefaultBlur: () => this.trigger('clipboard_blur'), }); } @@ -90,40 +86,34 @@ export class App extends DisposableWithEvents { const isHelpPaneVisible = ko.observable(false); const helpDiv = document.body.appendChild( - dom( - "div.g-help", + dom('div.g-help', dom.show(isHelpPaneVisible), // Toggle visibility dynamically - dom( - "table.g-help-table", - dom( - "thead", - dom("tr", dom("th", t("Key")), dom("th", t("Description"))), + dom('table.g-help-table', + dom('thead', + dom('tr', + dom('th', t("Key")), + dom('th', t("Description")) + ) ), dom.forEach(commandList.groups, (group) => { - const cmds = group.commands.filter((cmd) => - Boolean(cmd.desc && cmd.keys.length && !cmd.deprecated), - ); - return cmds.length > 0 - ? dom( - "tbody", - dom("tr", dom("td", { colspan: "2" }, group.group)), - dom.forEach(cmds, (cmd) => - dom( - "tr", - dom("td", commands.allCommands[cmd.name]!.getKeysDom()), - dom("td", cmd.desc), - ), - ), + const cmds = group.commands.filter((cmd) => Boolean(cmd.desc && cmd.keys.length && !cmd.deprecated)); + return cmds.length > 0 ? + dom('tbody', + dom('tr', + dom('td', {colspan: '2'}, group.group) + ), + dom.forEach(cmds, (cmd) => + dom('tr', + dom('td', commands.allCommands[cmd.name]!.getKeysDom()), + dom('td', cmd.desc) + ) ) - : null; - }), - ), - ), + ) : null; + }) + ) + ) ); - this.onDispose(() => { - dom.domDispose(helpDiv); - helpDiv.remove(); - }); + this.onDispose(() => { dom.domDispose(helpDiv); helpDiv.remove(); }); /** Click outside the popup to close it */ document.addEventListener("click", function (event) { @@ -133,73 +123,30 @@ export class App extends DisposableWithEvents { }); /** Use "Cmd + /" to toggle */ - this.autoDispose( - commands.createGroup( - { - help() { - G.window.open("help", "_blank").focus(); - }, - shortcuts() { - isHelpPaneVisible(!isHelpPaneVisible()); - }, // FIXED: Toggle Open/Close - historyBack() { - G.window.history.back(); - }, - historyForward() { - G.window.history.forward(); - }, - }, - this, - true, - ), - ); + this.autoDispose(commands.createGroup({ + help() { G.window.open('help', '_blank').focus(); }, + shortcuts() { isHelpPaneVisible(!isHelpPaneVisible()); }, // FIXED: Toggle Open/Close + historyBack() { G.window.history.back(); }, + historyForward() { G.window.history.forward(); }, + }, this, true)); /** Ensure menu closes on cancel */ - this.autoDispose( - commands.createGroup( - { - cancel() { - isHelpPaneVisible(false); - }, // Close menu when Esc/Cancel is triggered - cursorDown() { - helpDiv.scrollBy(0, 30); - }, - cursorUp() { - helpDiv.scrollBy(0, -30); - }, - pageUp() { - helpDiv.scrollBy(0, -helpDiv.clientHeight); - }, - pageDown() { - helpDiv.scrollBy(0, helpDiv.clientHeight); - }, - moveToFirstField() { - helpDiv.scrollTo(0, 0); - }, // home - moveToLastField() { - helpDiv.scrollTo(0, helpDiv.scrollHeight); - }, // end - find() { - return true; - }, // restore browser search - help() { - isHelpPaneVisible(false); - }, // Close menu - }, - this, - isHelpPaneVisible, - ), - ); - - this.listenTo(this.comm, "clientConnect", (message) => { - console.log( - `App clientConnect event: needReload ${message.needReload} version ${message.serverVersion}`, - ); + this.autoDispose(commands.createGroup({ + cancel() { isHelpPaneVisible(false); }, // Close menu when Esc/Cancel is triggered + cursorDown() { helpDiv.scrollBy(0, 30); }, + cursorUp() { helpDiv.scrollBy(0, -30); }, + pageUp() { helpDiv.scrollBy(0, -helpDiv.clientHeight); }, + pageDown() { helpDiv.scrollBy(0, helpDiv.clientHeight); }, + moveToFirstField() { helpDiv.scrollTo(0, 0); }, // home + moveToLastField() { helpDiv.scrollTo(0, helpDiv.scrollHeight); }, // end + find() { return true; }, // restore browser search + help() { isHelpPaneVisible(false); }, // Close menu + }, this, isHelpPaneVisible)); + + this.listenTo(this.comm, 'clientConnect', (message) => { + console.log(`App clientConnect event: needReload ${message.needReload} version ${message.serverVersion}`); this._settings(message.settings); - if ( - message.serverVersion === "dead" || - (this._serverVersion && this._serverVersion !== message.serverVersion) - ) { + if (message.serverVersion === 'dead' || (this._serverVersion && this._serverVersion !== message.serverVersion)) { console.log("Upgrading..."); // Server has upgraded. Upgrade client. TODO: be gentle and polite. return this.reload(); @@ -212,24 +159,24 @@ export class App extends DisposableWithEvents { } }); - this.listenTo(this.comm, "connectState", (isConnected: boolean) => { + this.listenTo(this.comm, 'connectState', (isConnected: boolean) => { this.topAppModel.notifier.setConnectState(isConnected); }); - this.listenTo(this.comm, "docShutdown", () => { + this.listenTo(this.comm, 'docShutdown', () => { console.log("Received docShutdown"); // Reload on next tick, to let other objects process 'docShutdown' before they get disposed. setTimeout(() => this.reloadPane(), 0); }); - this.listenTo(this.comm, "docError", (msg: CommDocError) => { + this.listenTo(this.comm, 'docError', (msg: CommDocError) => { this._checkError(new Error(msg.data.message)); }); // When the document is unloaded, dispose the app, allowing it to do any needed // cleanup (e.g. Document on disposal triggers closeDoc message to the server). It needs to be // in 'beforeunload' rather than 'unload', since websocket is closed by the time of 'unload'. - G.window.addEventListener("beforeunload", (ev: BeforeUnloadEvent) => { + G.window.addEventListener('beforeunload', (ev: BeforeUnloadEvent) => { if (unsavedChanges.haveUnsavedChanges()) { // Following https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event ev.returnValue = true; @@ -251,9 +198,7 @@ export class App extends DisposableWithEvents { // We want to test errors from Selenium, but errors we can trigger using driver.executeScript() // will be impossible for the application to report properly (they seem to be considered not of // "same-origin"). So this silly callback is for tests to generate a fake error. - public testTriggerError(msg: string) { - throw new Error(msg); - } + public testTriggerError(msg: string) { throw new Error(msg); } public reloadPane() { console.log("reloadPane"); @@ -291,16 +236,12 @@ export class App extends DisposableWithEvents { * is available in weblate and good translations have been updated. */ public checkSpecialTranslationKey() { - return t( - "Translators: please translate this only when your language is ready to be offered to users", - ); + return t('Translators: please translate this only when your language is ready to be offered to users'); } // Get the user profile for testing purposes public async testGetProfile(): Promise { - const resp = await fetchFromHome("/api/profile/user", { - credentials: "include", - }); + const resp = await fetchFromHome('/api/profile/user', {credentials: 'include'}); return resp.json(); } From 37b79d9b259b6fa24874a937115daea63944dae0 Mon Sep 17 00:00:00 2001 From: seven-of-eleven Date: Wed, 26 Feb 2025 17:36:05 -0500 Subject: [PATCH 03/13] Put back the #grist-logo-wrapper code I had unintentionally removed, restored and reomved some comments as requested, and code adjustments as requested. Added onClickOutside function to domUtils.ts to dismiss the help window. Restored DocApiTypes-ti.ts as it was unrelated to this request. --- app/client/lib/domUtils.ts | 17 +++++++++++++++++ app/client/ui/App.ts | 18 +++++++----------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/app/client/lib/domUtils.ts b/app/client/lib/domUtils.ts index fa0cf94e38..7079dd24aa 100644 --- a/app/client/lib/domUtils.ts +++ b/app/client/lib/domUtils.ts @@ -86,3 +86,20 @@ export function domDispatch(element: Element, name: string, args?: any) { detail: args })); } + +// Helper binding function to handle click outside an element. Takes into account floating menus. +export function onClickOutside(click: () => void) { + return (content: HTMLElement) => { + const onClick = (evt: MouseEvent) => { + const target: Node | null = evt.target as Node; + if (target && !content.contains(target)) { + // Check if any parent of target has class grist-floating-menu, if so, don't close. + if (target.parentElement?.closest(".grist-floating-menu")) { + return; + } + click(); + } + }; + dom.autoDisposeElem(content, dom.onElem(document, 'click', onClick, {useCapture: true})); + }; +} \ No newline at end of file diff --git a/app/client/ui/App.ts b/app/client/ui/App.ts index 689cd27232..c146e714d4 100644 --- a/app/client/ui/App.ts +++ b/app/client/ui/App.ts @@ -23,6 +23,7 @@ import {ISupportedFeatures} from 'app/common/UserConfig'; import {dom} from 'grainjs'; import * as ko from 'knockout'; import {makeT} from 'app/client/lib/localization'; +import { onClickOutside } from '../lib/domUtils'; const t = makeT('App'); @@ -85,8 +86,11 @@ export class App extends DisposableWithEvents { const isHelpPaneVisible = ko.observable(false); + G.document.querySelector('#grist-logo-wrapper')?.remove(); + const helpDiv = document.body.appendChild( dom('div.g-help', + onClickOutside(() => isHelpPaneVisible(false)), dom.show(isHelpPaneVisible), // Toggle visibility dynamically dom('table.g-help-table', dom('thead', @@ -115,17 +119,9 @@ export class App extends DisposableWithEvents { ); this.onDispose(() => { dom.domDispose(helpDiv); helpDiv.remove(); }); - /** Click outside the popup to close it */ - document.addEventListener("click", function (event) { - if (isHelpPaneVisible() && !helpDiv.contains(event.target as Node)) { - isHelpPaneVisible(false); // Hide the help menu - } - }); - - /** Use "Cmd + /" to toggle */ this.autoDispose(commands.createGroup({ help() { G.window.open('help', '_blank').focus(); }, - shortcuts() { isHelpPaneVisible(!isHelpPaneVisible()); }, // FIXED: Toggle Open/Close + shortcuts() { isHelpPaneVisible(true); }, historyBack() { G.window.history.back(); }, historyForward() { G.window.history.forward(); }, }, this, true)); @@ -133,14 +129,14 @@ export class App extends DisposableWithEvents { /** Ensure menu closes on cancel */ this.autoDispose(commands.createGroup({ cancel() { isHelpPaneVisible(false); }, // Close menu when Esc/Cancel is triggered - cursorDown() { helpDiv.scrollBy(0, 30); }, + cursorDown() { helpDiv.scrollBy(0, 30); }, // 30 is height of the row in the help screen cursorUp() { helpDiv.scrollBy(0, -30); }, pageUp() { helpDiv.scrollBy(0, -helpDiv.clientHeight); }, pageDown() { helpDiv.scrollBy(0, helpDiv.clientHeight); }, moveToFirstField() { helpDiv.scrollTo(0, 0); }, // home moveToLastField() { helpDiv.scrollTo(0, helpDiv.scrollHeight); }, // end find() { return true; }, // restore browser search - help() { isHelpPaneVisible(false); }, // Close menu + shortcuts() { isHelpPaneVisible(false); }, // Close menu }, this, isHelpPaneVisible)); this.listenTo(this.comm, 'clientConnect', (message) => { From aa20b5c8bb2d421825ce8d529bc493f7f36227a8 Mon Sep 17 00:00:00 2001 From: seven-of-eleven Date: Thu, 27 Feb 2025 17:01:09 -0500 Subject: [PATCH 04/13] Added a checkbox to Grid Options to hide field icons in tables. The icons can only be hidden in table view, reappear when the field is edited. --- app/client/components/GridView.js | 7 +++++-- app/client/models/entities/ViewSectionRec.ts | 1 + app/client/ui/GridOptions.ts | 6 ++++++ app/client/widgets/TextBox.css | 4 ++-- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/app/client/components/GridView.js b/app/client/components/GridView.js index f64f27a0a8..747c57e48f 100644 --- a/app/client/components/GridView.js +++ b/app/client/components/GridView.js @@ -1150,6 +1150,7 @@ GridView.prototype.buildDom = function() { let vHorizontalGridlines = v.optionsObj.prop('horizontalGridlines'); let vVerticalGridlines = v.optionsObj.prop('verticalGridlines'); let vZebraStripes = v.optionsObj.prop('zebraStripes'); + let vFieldIcon = v.optionsObj.prop('fieldIcon'); var renameCommands = { nextField: function() { @@ -1559,8 +1560,10 @@ GridView.prototype.buildDom = function() { //a cell in that row becomes larger kd.style('borderRightWidth', v.borderWidthPx), kd.toggleClass('selected', isSelected), - // Optional icon. Currently only use to show formula icon. - dom('div.field-icon'), + // Optional icon. Currently only use to show formula icon. Can be hidden from within the UI by "Grid Options" + dom('div.field-icon', + kd.toggleClass('record-icon', vFieldIcon), + ), fieldBuilder.buildDomWithCursor(row, isCellActive, isCellSelected), dom('div.selection'), ); diff --git a/app/client/models/entities/ViewSectionRec.ts b/app/client/models/entities/ViewSectionRec.ts index 714b4a3c16..4f4e2f3141 100644 --- a/app/client/models/entities/ViewSectionRec.ts +++ b/app/client/models/entities/ViewSectionRec.ts @@ -398,6 +398,7 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel): const defaultOptions = { verticalGridlines: true, horizontalGridlines: true, + fieldIcon: true, // field icons are enable by default to be consistent with the default app behaviour zebraStripes: false, customView: '', numFrozen: 0 diff --git a/app/client/ui/GridOptions.ts b/app/client/ui/GridOptions.ts index 2afc118027..e32fd1aee1 100644 --- a/app/client/ui/GridOptions.ts +++ b/app/client/ui/GridOptions.ts @@ -34,6 +34,12 @@ export class GridOptions extends Disposable { testId('h-grid-button') ), + cssRow( + checkbox(setSaveValueFromKo(this, section.optionsObj.prop('fieldIcon'))), + t("Field Icons"), + testId('field-icon-button'), + ), + cssRow( checkbox(setSaveValueFromKo(this, section.optionsObj.prop('zebraStripes'))), t("Zebra Stripes"), diff --git a/app/client/widgets/TextBox.css b/app/client/widgets/TextBox.css index 9229805a7c..e4c596b52e 100644 --- a/app/client/widgets/TextBox.css +++ b/app/client/widgets/TextBox.css @@ -3,14 +3,14 @@ } @media not print { - .formula_field, .formula_field_edit { + .formula_field:has(.record-icon), .formula_field_edit { padding-left: 18px; } .formula_field_edit { color: #D0D0D0; } - .formula_field .field-icon, + .formula_field .record-icon, .formula_field_edit::before, .formula_field_sidepane::before { /* based on standard icon styles */ From 9c9f8096dae50e588e2261e4bfda51aedfb345a9 Mon Sep 17 00:00:00 2001 From: seven-of-eleven Date: Thu, 27 Feb 2025 17:08:30 -0500 Subject: [PATCH 05/13] Added new line to end of domUtils.ts file. Changed import to absolute path for onClickOutside. --- app/client/lib/domUtils.ts | 2 +- app/client/ui/App.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/client/lib/domUtils.ts b/app/client/lib/domUtils.ts index 7079dd24aa..55aca7d427 100644 --- a/app/client/lib/domUtils.ts +++ b/app/client/lib/domUtils.ts @@ -102,4 +102,4 @@ export function onClickOutside(click: () => void) { }; dom.autoDisposeElem(content, dom.onElem(document, 'click', onClick, {useCapture: true})); }; -} \ No newline at end of file +} diff --git a/app/client/ui/App.ts b/app/client/ui/App.ts index c146e714d4..5cb1c8dc57 100644 --- a/app/client/ui/App.ts +++ b/app/client/ui/App.ts @@ -23,7 +23,7 @@ import {ISupportedFeatures} from 'app/common/UserConfig'; import {dom} from 'grainjs'; import * as ko from 'knockout'; import {makeT} from 'app/client/lib/localization'; -import { onClickOutside } from '../lib/domUtils'; +import { onClickOutside } from 'app/client/lib/domUtils'; const t = makeT('App'); From 27bf0dbcaf620d7e6b03fa795d2f270e1835771e Mon Sep 17 00:00:00 2001 From: Seven <76855829+seven-of-eleven@users.noreply.github.com> Date: Thu, 27 Feb 2025 23:02:32 -0500 Subject: [PATCH 06/13] Additional line on domUtils.ts file didn't stick. Added again. --- app/client/lib/domUtils.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/client/lib/domUtils.ts b/app/client/lib/domUtils.ts index 55aca7d427..57266fbbae 100644 --- a/app/client/lib/domUtils.ts +++ b/app/client/lib/domUtils.ts @@ -1,5 +1,5 @@ -import {useBindable} from 'app/common/gutil'; -import {BindableValue, Computed, dom, IDisposableOwner, Observable, UseCB} from 'grainjs'; +import { useBindable } from 'app/common/gutil'; +import { BindableValue, Computed, dom, IDisposableOwner, Observable, UseCB } from 'grainjs'; /** * Version of makeTestId that can be appended conditionally. @@ -28,7 +28,7 @@ export function autoSelect() { */ export const AsyncComputed = { create(owner: IDisposableOwner, cb: (use: UseCB) => Promise): AsyncComputed { - const backend: Observable = Observable.create(owner, undefined); + const backend: Observable = Observable.create(owner, undefined); const dirty = Observable.create(owner, true); const computed: Computed> = Computed.create(owner, cb as any); let ticket = 0; @@ -49,7 +49,7 @@ export const AsyncComputed = { }); } }; -export interface AsyncComputed extends Observable { +export interface AsyncComputed extends Observable { /** * Whether computed wasn't updated yet. */ @@ -100,6 +100,7 @@ export function onClickOutside(click: () => void) { click(); } }; - dom.autoDisposeElem(content, dom.onElem(document, 'click', onClick, {useCapture: true})); + dom.autoDisposeElem(content, dom.onElem(document, 'click', onClick, { useCapture: true })); }; } + From 05d2f29bf42853a0deec709a227f896c91fafce5 Mon Sep 17 00:00:00 2001 From: Seven <76855829+seven-of-eleven@users.noreply.github.com> Date: Thu, 27 Feb 2025 23:39:49 -0500 Subject: [PATCH 07/13] Additional line on domUtils.ts file again but without the other code formatting changes. --- app/client/lib/domUtils.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/client/lib/domUtils.ts b/app/client/lib/domUtils.ts index 57266fbbae..55aca7d427 100644 --- a/app/client/lib/domUtils.ts +++ b/app/client/lib/domUtils.ts @@ -1,5 +1,5 @@ -import { useBindable } from 'app/common/gutil'; -import { BindableValue, Computed, dom, IDisposableOwner, Observable, UseCB } from 'grainjs'; +import {useBindable} from 'app/common/gutil'; +import {BindableValue, Computed, dom, IDisposableOwner, Observable, UseCB} from 'grainjs'; /** * Version of makeTestId that can be appended conditionally. @@ -28,7 +28,7 @@ export function autoSelect() { */ export const AsyncComputed = { create(owner: IDisposableOwner, cb: (use: UseCB) => Promise): AsyncComputed { - const backend: Observable = Observable.create(owner, undefined); + const backend: Observable = Observable.create(owner, undefined); const dirty = Observable.create(owner, true); const computed: Computed> = Computed.create(owner, cb as any); let ticket = 0; @@ -49,7 +49,7 @@ export const AsyncComputed = { }); } }; -export interface AsyncComputed extends Observable { +export interface AsyncComputed extends Observable { /** * Whether computed wasn't updated yet. */ @@ -100,7 +100,6 @@ export function onClickOutside(click: () => void) { click(); } }; - dom.autoDisposeElem(content, dom.onElem(document, 'click', onClick, { useCapture: true })); + dom.autoDisposeElem(content, dom.onElem(document, 'click', onClick, {useCapture: true})); }; } - From 5b7f3fdc23186f2731ac468f98a60e8dc332866e Mon Sep 17 00:00:00 2001 From: seven-of-eleven Date: Thu, 6 Mar 2025 17:26:49 -0500 Subject: [PATCH 08/13] Updated the CSS to fix icon issues in Record widget and formula editing --- app/client/ui/GridOptions.ts | 2 +- app/client/widgets/TextBox.css | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/client/ui/GridOptions.ts b/app/client/ui/GridOptions.ts index e32fd1aee1..873832379d 100644 --- a/app/client/ui/GridOptions.ts +++ b/app/client/ui/GridOptions.ts @@ -36,7 +36,7 @@ export class GridOptions extends Disposable { cssRow( checkbox(setSaveValueFromKo(this, section.optionsObj.prop('fieldIcon'))), - t("Field Icons"), + t("Formula Icons"), testId('field-icon-button'), ), diff --git a/app/client/widgets/TextBox.css b/app/client/widgets/TextBox.css index e4c596b52e..906cb31c43 100644 --- a/app/client/widgets/TextBox.css +++ b/app/client/widgets/TextBox.css @@ -3,7 +3,9 @@ } @media not print { - .formula_field:has(.record-icon), .formula_field_edit { + .formula_field:has(.record-icon), + .formula_field_edit:has(> .record-icon), + .g_record_detail_value.formula_field:has(> .field-icon) { padding-left: 18px; } .formula_field_edit { @@ -11,7 +13,9 @@ } .formula_field .record-icon, - .formula_field_edit::before, + .g_record_detail_value.formula_field .field-icon, + .formula_field_edit:has(> .record-icon)::before, + .formula_editor.formula_field_edit::before, .formula_field_sidepane::before { /* based on standard icon styles */ content: ""; @@ -30,6 +34,7 @@ cursor: pointer; } + .g_record_detail_value.formula_field .field-icon, .formula_field .field-icon, .formula_field_edit::before { background-color: #D0D0D0; } From a18aa8db2ff4388f45c6cfc80b867b75de336e60 Mon Sep 17 00:00:00 2001 From: seven-of-eleven Date: Fri, 7 Mar 2025 13:11:13 -0500 Subject: [PATCH 09/13] Updated the CSS to ensure proper colors and transitions when editing a cell/field, and renamed the fieldIcon property to formulaIcon for clarity (as there is already a field-icon class). --- app/client/components/GridView.js | 4 ++-- app/client/models/entities/ViewSectionRec.ts | 2 +- app/client/ui/GridOptions.ts | 4 ++-- app/client/widgets/TextBox.css | 1 + 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/client/components/GridView.js b/app/client/components/GridView.js index 747c57e48f..dcdacf0add 100644 --- a/app/client/components/GridView.js +++ b/app/client/components/GridView.js @@ -1150,7 +1150,7 @@ GridView.prototype.buildDom = function() { let vHorizontalGridlines = v.optionsObj.prop('horizontalGridlines'); let vVerticalGridlines = v.optionsObj.prop('verticalGridlines'); let vZebraStripes = v.optionsObj.prop('zebraStripes'); - let vFieldIcon = v.optionsObj.prop('fieldIcon'); + let vFormulaIcon = v.optionsObj.prop('formulaIcon'); var renameCommands = { nextField: function() { @@ -1562,7 +1562,7 @@ GridView.prototype.buildDom = function() { kd.toggleClass('selected', isSelected), // Optional icon. Currently only use to show formula icon. Can be hidden from within the UI by "Grid Options" dom('div.field-icon', - kd.toggleClass('record-icon', vFieldIcon), + kd.toggleClass('record-icon', vFormulaIcon), ), fieldBuilder.buildDomWithCursor(row, isCellActive, isCellSelected), dom('div.selection'), diff --git a/app/client/models/entities/ViewSectionRec.ts b/app/client/models/entities/ViewSectionRec.ts index 4f4e2f3141..e8574ed6dd 100644 --- a/app/client/models/entities/ViewSectionRec.ts +++ b/app/client/models/entities/ViewSectionRec.ts @@ -398,7 +398,7 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel): const defaultOptions = { verticalGridlines: true, horizontalGridlines: true, - fieldIcon: true, // field icons are enable by default to be consistent with the default app behaviour + formulaIcon: true, // field icons are enable by default to be consistent with the default app behaviour zebraStripes: false, customView: '', numFrozen: 0 diff --git a/app/client/ui/GridOptions.ts b/app/client/ui/GridOptions.ts index 873832379d..49ae427cee 100644 --- a/app/client/ui/GridOptions.ts +++ b/app/client/ui/GridOptions.ts @@ -35,9 +35,9 @@ export class GridOptions extends Disposable { ), cssRow( - checkbox(setSaveValueFromKo(this, section.optionsObj.prop('fieldIcon'))), + checkbox(setSaveValueFromKo(this, section.optionsObj.prop('formulaIcon'))), t("Formula Icons"), - testId('field-icon-button'), + testId('formula-icon-button'), ), cssRow( diff --git a/app/client/widgets/TextBox.css b/app/client/widgets/TextBox.css index 906cb31c43..112baf3987 100644 --- a/app/client/widgets/TextBox.css +++ b/app/client/widgets/TextBox.css @@ -38,6 +38,7 @@ .formula_field .field-icon, .formula_field_edit::before { background-color: #D0D0D0; } + .transform_field.formula_field > .field-icon, .formula_field_edit:not(.readonly)::before { background-color: var(--grist-color-cursor); } From d9d18dd5ad71956770d71d8b5d2659908f462766 Mon Sep 17 00:00:00 2001 From: seven-of-eleven Date: Fri, 7 Mar 2025 13:31:40 -0500 Subject: [PATCH 10/13] Updated a couple of comments for clarity. --- app/client/components/GridView.js | 6 ++++-- app/client/models/entities/ViewSectionRec.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/client/components/GridView.js b/app/client/components/GridView.js index dcdacf0add..9b86c93fed 100644 --- a/app/client/components/GridView.js +++ b/app/client/components/GridView.js @@ -1560,9 +1560,11 @@ GridView.prototype.buildDom = function() { //a cell in that row becomes larger kd.style('borderRightWidth', v.borderWidthPx), kd.toggleClass('selected', isSelected), - // Optional icon. Currently only use to show formula icon. Can be hidden from within the UI by "Grid Options" + // The field-icon is presently only used for "=" in formula fields. + // The record-icon class isolates the "=" being applied to the formula fields. + // Allowing additional field icons to be added as field-icon class if desired. dom('div.field-icon', - kd.toggleClass('record-icon', vFormulaIcon), + kd.toggleClass('record-icon', vFormulaIcon), // Also grabbed from v.optionsObj at start of GridView buildDom ), fieldBuilder.buildDomWithCursor(row, isCellActive, isCellSelected), dom('div.selection'), diff --git a/app/client/models/entities/ViewSectionRec.ts b/app/client/models/entities/ViewSectionRec.ts index e8574ed6dd..4f9fd5d905 100644 --- a/app/client/models/entities/ViewSectionRec.ts +++ b/app/client/models/entities/ViewSectionRec.ts @@ -398,7 +398,7 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel): const defaultOptions = { verticalGridlines: true, horizontalGridlines: true, - formulaIcon: true, // field icons are enable by default to be consistent with the default app behaviour + formulaIcon: true, // formula icons are enable by default consistent with the default app behaviour zebraStripes: false, customView: '', numFrozen: 0 From 7b40eed7e48f632bd74a8e407d1e1f71627f70ae Mon Sep 17 00:00:00 2001 From: seven-of-eleven Date: Mon, 10 Mar 2025 13:49:55 -0400 Subject: [PATCH 11/13] Took a crack at updating the test for GridOptions.ntest.js. --- test/nbrowser/GridOptions.ntest.js | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/test/nbrowser/GridOptions.ntest.js b/test/nbrowser/GridOptions.ntest.js index 1e3045b7fc..40f9f973e9 100644 --- a/test/nbrowser/GridOptions.ntest.js +++ b/test/nbrowser/GridOptions.ntest.js @@ -19,12 +19,12 @@ describe("GridOptions.ntest", function() { /* Test that styles on the given section match the specified flags * sec: index into secNames - * hor/vert/zebra: boolean flags + * hor/vert/icon/zebra: boolean flags */ - async function assertHVZ(sec, hor, vert, zebra) { + async function assertHVCZ(sec, hor, vert, icon, zebra) { let testClasses = - ['record-hlines', 'record-vlines', 'record-zebra']; - let flags = [hor, vert, zebra]; + ['record-hlines', 'record-vlines', 'record-icon', 'record-zebra']; + let flags = [hor, vert, icon, zebra]; let cell = await gu.getCell({rowNum: 1, col: 0, section: secNames[sec]}); let row = await cell.findClosest('.record'); @@ -84,10 +84,11 @@ describe("GridOptions.ntest", function() { // get handles on elements let h = ".test-h-grid-button input"; let v = ".test-v-grid-button input"; + let c = ".test-formula-icon-button input"; let z = ".test-zebra-stripe-button input"; // should start with v+h gridlines, no zebra - await assertHVZ(0, true, true, false); + await assertHVCZ(0, true, true, true, false); // change values on all the sections await switchTo(0); @@ -96,24 +97,26 @@ describe("GridOptions.ntest", function() { await switchTo(1); await $(h).click(); await $(v).click(); + await $(c).click(); await switchTo(2); await $(h).click(); // turn off + await $(c).click(); // turn off await $(z).click(); // turn on await gu.waitForServer(); - await assertHVZ(0, true, true, true); // all on - await assertHVZ(1, false, false, false); // all off - await assertHVZ(2, false, true, true); // -h +v +z + await assertHVCZ(0, true, true, true, true); // all on + await assertHVCZ(1, false, false, false, false); // all off + await assertHVCZ(2, false, true, false, true); // -h +v -c +z // ensure that values persist after reload await driver.navigate().refresh(); //await $.injectIntoPage(); await gu.waitForDocToLoad(); await gu.hideBanners(); - await assertHVZ(0, true, true, true); // all on - await assertHVZ(1, false, false, false); // all off - await assertHVZ(2, false, true, true); // -h +v +z + await assertHVCZ(0, true, true, true, true); // all on + await assertHVCZ(1, false, false, false, false); // all off + await assertHVCZ(2, false, true, false, true); // -h +v -c +z }); From 62b45b4564372f67abcdc147f9a21192b7c6a64c Mon Sep 17 00:00:00 2001 From: seven-of-eleven Date: Mon, 10 Mar 2025 16:04:03 -0400 Subject: [PATCH 12/13] Removed trailing space from line 1564. --- app/client/components/GridView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/client/components/GridView.js b/app/client/components/GridView.js index 9b86c93fed..2ec5367d13 100644 --- a/app/client/components/GridView.js +++ b/app/client/components/GridView.js @@ -1561,7 +1561,7 @@ GridView.prototype.buildDom = function() { kd.style('borderRightWidth', v.borderWidthPx), kd.toggleClass('selected', isSelected), // The field-icon is presently only used for "=" in formula fields. - // The record-icon class isolates the "=" being applied to the formula fields. + // The record-icon class isolates the "=" being applied to the formula fields. // Allowing additional field icons to be added as field-icon class if desired. dom('div.field-icon', kd.toggleClass('record-icon', vFormulaIcon), // Also grabbed from v.optionsObj at start of GridView buildDom From a39493809a0db0d7c461856ba75f9056bf3c4381 Mon Sep 17 00:00:00 2001 From: Seven <76855829+seven-of-eleven@users.noreply.github.com> Date: Mon, 10 Mar 2025 21:19:47 -0400 Subject: [PATCH 13/13] Update GridOptions.ntest.js file. The test was passing on my laptop somehow. Hopefully this is fixes it. --- test/nbrowser/GridOptions.ntest.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/nbrowser/GridOptions.ntest.js b/test/nbrowser/GridOptions.ntest.js index 40f9f973e9..37ac938d00 100644 --- a/test/nbrowser/GridOptions.ntest.js +++ b/test/nbrowser/GridOptions.ntest.js @@ -23,8 +23,8 @@ describe("GridOptions.ntest", function() { */ async function assertHVCZ(sec, hor, vert, icon, zebra) { let testClasses = - ['record-hlines', 'record-vlines', 'record-icon', 'record-zebra']; - let flags = [hor, vert, icon, zebra]; + ['record-hlines', 'record-vlines', 'record-zebra']; + let flags = [hor, vert, zebra]; let cell = await gu.getCell({rowNum: 1, col: 0, section: secNames[sec]}); let row = await cell.findClosest('.record'); @@ -33,6 +33,11 @@ describe("GridOptions.ntest", function() { if(flags[i]) { assert.include(rowClasses, cls);} else { assert.notInclude(rowClasses, cls); } }); + // Check for the presence of 'record-icon' class on the div.field-icon element + const fieldIcon = await row.find('.field-icon'); + const iconClasses = await fieldIcon.classList(); + if (icon) { assert.include(iconClasses, 'record-icon'); } + else { assert.notInclude(iconClasses, 'record-icon'); } }