(core) Removing GRIST_FORMULA_ASSISTANT flag

Summary:
A floating formula editor is available by default and in the basic setup allows just formula modification.
AI assistant is now an optional component of the floating editor and it is controlled by OPENAPI_KEY presence.
Env variable GRIST_FORMULA_ASSISTANT was removed, new feature flag HAS_FORMULA_ASSISTANT is derived from the presence of OPENAPI_KEY.

Also updated anonymous signup nudge. By default it displays only info that this feature is only for logged in users.

Test Plan: updated

Reviewers: georgegevoian

Reviewed By: georgegevoian

Differential Revision: https://phab.getgrist.com/D3987
pull/617/head
Jarosław Sadziński 10 months ago
parent 24bbf375f9
commit 732611c356

@ -302,7 +302,7 @@ PORT | port number to listen on for Grist server
REDIS_URL | optional redis server for browser sessions and db query caching REDIS_URL | optional redis server for browser sessions and db query caching
GRIST_SNAPSHOT_TIME_CAP | optional. Define the caps for tracking buckets. Usage: {"hour": 25, "day": 32, "isoWeek": 12, "month": 96, "year": 1000} GRIST_SNAPSHOT_TIME_CAP | optional. Define the caps for tracking buckets. Usage: {"hour": 25, "day": 32, "isoWeek": 12, "month": 96, "year": 1000}
GRIST_SNAPSHOT_KEEP | optional. Number of recent snapshots to retain unconditionally for a document, regardless of when they were made GRIST_SNAPSHOT_KEEP | optional. Number of recent snapshots to retain unconditionally for a document, regardless of when they were made
OPENAI_API_KEY | optional. Used for the AI formula assistant. Sign up for an account on OpenAI and then generate a secret key [here](https://platform.openai.com/account/api-keys). You also need to set `GRIST_FORMULA_ASSISTANT=1`. OPENAI_API_KEY | optional. Used for the AI formula assistant. Sign up for an account on OpenAI and then generate a secret key [here](https://platform.openai.com/account/api-keys).
Sandbox related variables: Sandbox related variables:

@ -3,6 +3,9 @@ import {get as getBrowserGlobals} from 'app/client/lib/browserGlobals';
import {localStorageBoolObs} from 'app/client/lib/localStorageObs'; import {localStorageBoolObs} from 'app/client/lib/localStorageObs';
import {Observable} from 'grainjs'; import {Observable} from 'grainjs';
/**
* Are comments enabled by feature flag.
*/
export function COMMENTS(): Observable<boolean> { export function COMMENTS(): Observable<boolean> {
const G = getBrowserGlobals('document', 'window'); const G = getBrowserGlobals('document', 'window');
if (!G.window.COMMENTS) { if (!G.window.COMMENTS) {
@ -11,11 +14,9 @@ export function COMMENTS(): Observable<boolean> {
return G.window.COMMENTS; return G.window.COMMENTS;
} }
export function GRIST_FORMULA_ASSISTANT(): Observable<boolean> { /**
const G = getBrowserGlobals('document', 'window'); * Does backend supports AI assistant.
if (!G.window.GRIST_FORMULA_ASSISTANT) { */
G.window.GRIST_FORMULA_ASSISTANT = export function HAS_FORMULA_ASSISTANT() {
localStorageBoolObs('GRIST_FORMULA_ASSISTANT', Boolean(getGristConfig().featureFormulaAssistant)); return Boolean(getGristConfig().featureFormulaAssistant);
}
return G.window.GRIST_FORMULA_ASSISTANT;
} }

@ -1,29 +1,30 @@
import * as commands from 'app/client/components/commands'; import * as commands from 'app/client/components/commands';
import {GristDoc} from 'app/client/components/GristDoc'; import {GristDoc} from 'app/client/components/GristDoc';
import {logTelemetryEvent} from 'app/client/lib/telemetry';
import {makeT} from 'app/client/lib/localization'; import {makeT} from 'app/client/lib/localization';
import {localStorageBoolObs} from 'app/client/lib/localStorageObs'; import {localStorageBoolObs} from 'app/client/lib/localStorageObs';
import {movable} from 'app/client/lib/popupUtils';
import {logTelemetryEvent} from 'app/client/lib/telemetry';
import {ColumnRec, ViewFieldRec} from 'app/client/models/DocModel'; import {ColumnRec, ViewFieldRec} from 'app/client/models/DocModel';
import {ChatMessage} from 'app/client/models/entities/ColumnRec'; import {ChatMessage} from 'app/client/models/entities/ColumnRec';
import {GRIST_FORMULA_ASSISTANT} from 'app/client/models/features'; import {HAS_FORMULA_ASSISTANT} from 'app/client/models/features';
import {getLoginOrSignupUrl, urlState} from 'app/client/models/gristUrlState'; import {getLoginOrSignupUrl, urlState} from 'app/client/models/gristUrlState';
import {buildHighlightedCode} from 'app/client/ui/CodeHighlight'; import {buildHighlightedCode} from 'app/client/ui/CodeHighlight';
import {autoGrow} from 'app/client/ui/forms';
import {sanitizeHTML} from 'app/client/ui/sanitizeHTML'; import {sanitizeHTML} from 'app/client/ui/sanitizeHTML';
import {createUserImage} from 'app/client/ui/UserImage'; import {createUserImage} from 'app/client/ui/UserImage';
import {FormulaEditor} from 'app/client/widgets/FormulaEditor';
import {AssistanceResponse, AssistanceState, FormulaAssistanceContext} from 'app/common/AssistancePrompts';
import {basicButton, bigPrimaryButtonLink, primaryButton} from 'app/client/ui2018/buttons'; import {basicButton, bigPrimaryButtonLink, primaryButton} from 'app/client/ui2018/buttons';
import {theme, vars} from 'app/client/ui2018/cssVars'; import {theme, vars} from 'app/client/ui2018/cssVars';
import {autoGrow} from 'app/client/ui/forms';
import {icon} from 'app/client/ui2018/icons'; import {icon} from 'app/client/ui2018/icons';
import {cssLink} from 'app/client/ui2018/links'; import {cssLink} from 'app/client/ui2018/links';
import {commonUrls} from 'app/common/gristUrls';
import {movable} from 'app/client/lib/popupUtils';
import {loadingDots} from 'app/client/ui2018/loaders'; import {loadingDots} from 'app/client/ui2018/loaders';
import {menu, menuCssClass, menuItem} from 'app/client/ui2018/menus'; import {menu, menuCssClass, menuItem} from 'app/client/ui2018/menus';
import {FormulaEditor} from 'app/client/widgets/FormulaEditor';
import {AssistanceResponse, AssistanceState, FormulaAssistanceContext} from 'app/common/AssistancePrompts';
import {commonUrls} from 'app/common/gristUrls';
import {TelemetryEvent, TelemetryMetadata} from 'app/common/Telemetry'; import {TelemetryEvent, TelemetryMetadata} from 'app/common/Telemetry';
import {Computed, Disposable, dom, DomElementArg, makeTestId, import {getGristConfig} from 'app/common/urlUtils';
MutableObsArray, obsArray, Observable, styled, subscribeElem} from 'grainjs'; import {Computed, Disposable, dom, DomElementArg, makeTestId, MutableObsArray,
obsArray, Observable, styled, subscribeElem} from 'grainjs';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import noop from 'lodash/noop'; import noop from 'lodash/noop';
import {marked} from 'marked'; import {marked} from 'marked';
@ -54,8 +55,8 @@ export class FormulaAssistant extends Disposable {
`u:${this._options.gristDoc.appModel.currentUser?.id ?? 0};formulaAssistantExpanded`, true)); `u:${this._options.gristDoc.appModel.currentUser?.id ?? 0};formulaAssistantExpanded`, true));
/** Is the request pending */ /** Is the request pending */
private _waiting = Observable.create(this, false); private _waiting = Observable.create(this, false);
/** Is this feature enabled at all */ /** Is assistant features are enabled */
private _assistantEnabled: Computed<boolean>; private _assistantEnabled: boolean;
/** Preview column ref */ /** Preview column ref */
private _transformColRef: string; private _transformColRef: string;
/** Preview column id */ /** Preview column id */
@ -101,9 +102,7 @@ export class FormulaAssistant extends Disposable {
}) { }) {
super(); super();
this._assistantEnabled = Computed.create(this, use => { this._assistantEnabled = HAS_FORMULA_ASSISTANT();
return use(GRIST_FORMULA_ASSISTANT());
});
if (!this._options.field) { if (!this._options.field) {
// TODO: field is not passed only for rules (as there is no preview there available to the user yet) // TODO: field is not passed only for rules (as there is no preview there available to the user yet)
@ -192,16 +191,18 @@ export class FormulaAssistant extends Disposable {
this._buildChatPanel(), this._buildChatPanel(),
); );
if (!this._assistantExpanded.get()) { if (this._assistantEnabled) {
this._chatPanelBody.style.setProperty('height', '0px'); if (!this._assistantExpanded.get()) {
} else { this._chatPanelBody.style.setProperty('height', '0px');
// The actual height doesn't matter too much here, so we just pick } else {
// a value that guarantees the assistant will fill as much of the // The actual height doesn't matter too much here, so we just pick
// available space as possible. // a value that guarantees the assistant will fill as much of the
this._chatPanelBody.style.setProperty('height', '999px'); // available space as possible.
this._chatPanelBody.style.setProperty('height', '999px');
}
} }
if (this._assistantEnabled.get() && this._assistantExpanded.get()) { if (this._assistantEnabled && this._assistantExpanded.get()) {
this._logTelemetryEvent('assistantOpen', true); this._logTelemetryEvent('assistantOpen', true);
this._hasExpanded = true; this._hasExpanded = true;
} }
@ -590,18 +591,8 @@ export class FormulaAssistant extends Disposable {
* Builds the signup nudge shown to anonymous users at the bottom of the chat. * Builds the signup nudge shown to anonymous users at the bottom of the chat.
*/ */
private _buildSignupNudge() { private _buildSignupNudge() {
return cssSignupNudgeWrapper( const {deploymentType} = getGristConfig();
cssSignupNudgeParagraph( return deploymentType === 'saas' ? buildSignupNudge() : buildAnonNudge();
t('Sign up for a free Grist account to start using the Formula AI Assistant.'),
),
cssSignupNudgeButtonsRow(
bigPrimaryButtonLink(
t('Sign Up for Free'),
{href: getLoginOrSignupUrl()},
testId('ai-assistant-sign-up'),
),
),
);
} }
private async _handleChatEnterKeyDown(ev: KeyboardEvent) { private async _handleChatEnterKeyDown(ev: KeyboardEvent) {
@ -979,6 +970,30 @@ function buildAvatar(grist: GristDoc) {
} }
} }
function buildSignupNudge() {
return cssSignupNudgeWrapper(
cssSignupNudgeParagraph(
t('Sign up for a free Grist account to start using the Formula AI Assistant.'),
),
cssSignupNudgeButtonsRow(
bigPrimaryButtonLink(
t('Sign Up for Free'),
{href: getLoginOrSignupUrl()},
testId('ai-assistant-sign-up'),
),
),
);
}
function buildAnonNudge() {
return cssSignupNudgeWrapper(
cssSignupNudgeWrapper.cls('-center'),
cssSignupNudgeParagraph(
t('Formula AI Assistant is only available for logged in users.'),
),
);
}
const MIN_FORMULA_EDITOR_HEIGHT_PX = 100; const MIN_FORMULA_EDITOR_HEIGHT_PX = 100;
const FORMULA_EDITOR_BUTTONS_HEIGHT_PX = 42; const FORMULA_EDITOR_BUTTONS_HEIGHT_PX = 42;
@ -1275,6 +1290,11 @@ const cssSignupNudgeWrapper = styled('div', `
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
flex-direction: column; flex-direction: column;
&-center {
display: flex;
justify-content: center;
align-items: center;
}
`); `);
const cssSignupNudgeParagraph = styled('div', ` const cssSignupNudgeParagraph = styled('div', `

@ -7,7 +7,7 @@ import {DataRowModel} from 'app/client/models/DataRowModel';
import {ColumnRec} from 'app/client/models/DocModel'; import {ColumnRec} from 'app/client/models/DocModel';
import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec'; import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
import {reportError} from 'app/client/models/errors'; import {reportError} from 'app/client/models/errors';
import {GRIST_FORMULA_ASSISTANT} from 'app/client/models/features'; import {HAS_FORMULA_ASSISTANT} from 'app/client/models/features';
import {hoverTooltip} from 'app/client/ui/tooltips'; import {hoverTooltip} from 'app/client/ui/tooltips';
import {textButton} from 'app/client/ui2018/buttons'; import {textButton} from 'app/client/ui2018/buttons';
import {colors, testId, theme, vars} from 'app/client/ui2018/cssVars'; import {colors, testId, theme, vars} from 'app/client/ui2018/cssVars';
@ -53,7 +53,7 @@ export class FormulaEditor extends NewBaseEditor {
public isDetached = Observable.create(this, false); public isDetached = Observable.create(this, false);
protected options: IFormulaEditorOptions; protected options: IFormulaEditorOptions;
private _formulaEditor: any; private _aceEditor: any;
private _dom: HTMLElement; private _dom: HTMLElement;
private _editorPlacement!: EditorPlacement; private _editorPlacement!: EditorPlacement;
private _placementHolder = Holder.create(this); private _placementHolder = Holder.create(this);
@ -71,7 +71,7 @@ export class FormulaEditor extends NewBaseEditor {
this._isEmpty = Computed.create(this, this.editorState, (_use, state) => state === ''); this._isEmpty = Computed.create(this, this.editorState, (_use, state) => state === '');
this._formulaEditor = AceEditor.create({ this._aceEditor = AceEditor.create({
// A bit awkward, but we need to assume calcSize is not used until attach() has been called // A bit awkward, but we need to assume calcSize is not used until attach() has been called
// and _editorPlacement created. // and _editorPlacement created.
column: options.column, column: options.column,
@ -145,14 +145,14 @@ export class FormulaEditor extends NewBaseEditor {
// the DOM to update before resizing. // the DOM to update before resizing.
this.autoDispose(errorDetails.addListener(() => setTimeout(this.resize.bind(this), 0))); this.autoDispose(errorDetails.addListener(() => setTimeout(this.resize.bind(this), 0)));
this._canDetach = Boolean(GRIST_FORMULA_ASSISTANT().get() && options.canDetach && !options.readonly); this._canDetach = Boolean(options.canDetach && !options.readonly);
this.autoDispose(this._formulaEditor); this.autoDispose(this._aceEditor);
// Show placeholder text when the formula is blank. // Show placeholder text when the formula is blank.
this._isEmpty.addListener(() => this._updateEditorPlaceholder()); this._isEmpty.addListener(() => this._updateEditorPlaceholder());
// Update the placeholder text when expanding or collapsing the editor. // Disable undo/redo while the editor is detached.
this.isDetached.addListener((isDetached) => { this.isDetached.addListener((isDetached) => {
// TODO: look into whether we can support undo/redo while the editor is detached. // TODO: look into whether we can support undo/redo while the editor is detached.
if (isDetached) { if (isDetached) {
@ -160,8 +160,6 @@ export class FormulaEditor extends NewBaseEditor {
} else { } else {
options.gristDoc.getUndoStack().enable(); options.gristDoc.getUndoStack().enable();
} }
this._updateEditorPlaceholder();
}); });
this.onDispose(() => { this.onDispose(() => {
@ -203,22 +201,22 @@ export class FormulaEditor extends NewBaseEditor {
), ),
cssFormulaEditor.cls('-detached', this.isDetached), cssFormulaEditor.cls('-detached', this.isDetached),
dom('div.formula_editor.formula_field_edit', testId('formula-editor'), dom('div.formula_editor.formula_field_edit', testId('formula-editor'),
this._formulaEditor.buildDom((aceObj: any) => { this._aceEditor.buildDom((aceObj: any) => {
aceObj.setFontSize(11); aceObj.setFontSize(11);
aceObj.setHighlightActiveLine(false); aceObj.setHighlightActiveLine(false);
aceObj.getSession().setUseWrapMode(false); aceObj.getSession().setUseWrapMode(false);
aceObj.renderer.setPadding(0); aceObj.renderer.setPadding(0);
const val = initialValue; const val = initialValue;
const pos = Math.min(options.cursorPos, val.length); const pos = Math.min(options.cursorPos, val.length);
this._formulaEditor.setValue(val, pos); this._aceEditor.setValue(val, pos);
this._formulaEditor.attachCommandGroup(aceCommands); this._aceEditor.attachCommandGroup(aceCommands);
// enable formula editing if state was passed // enable formula editing if state was passed
if (options.state || options.readonly) { if (options.state || options.readonly) {
editingFormula(true); editingFormula(true);
} }
if (options.readonly) { if (options.readonly) {
this._formulaEditor.enable(false); this._aceEditor.enable(false);
aceObj.gotoLine(0, 0); // By moving, ace editor won't highlight anything aceObj.gotoLine(0, 0); // By moving, ace editor won't highlight anything
} }
// This catches any change to the value including e.g. via backspace or paste. // This catches any change to the value including e.g. via backspace or paste.
@ -238,7 +236,7 @@ export class FormulaEditor extends NewBaseEditor {
if (this.isDetached.get()) { return; } if (this.isDetached.get()) { return; }
if (errorDetails.get()){ if (errorDetails.get()){
hideErrDetails.set(!hideErrDetails.get()); hideErrDetails.set(!hideErrDetails.get());
this._formulaEditor.resize(); this._aceEditor.resize();
} }
}), }),
dom.maybe(errorDetails, () => dom.maybe(errorDetails, () =>
@ -249,7 +247,7 @@ export class FormulaEditor extends NewBaseEditor {
if (!this.isDetached.get()) { return; } if (!this.isDetached.get()) { return; }
if (errorDetails.get()){ if (errorDetails.get()){
hideErrDetails.set(!hideErrDetails.get()); hideErrDetails.set(!hideErrDetails.get());
this._formulaEditor.resize(); this._aceEditor.resize();
} }
}) })
)) ))
@ -281,9 +279,10 @@ export class FormulaEditor extends NewBaseEditor {
this._editorPlacement = EditorPlacement.create( this._editorPlacement = EditorPlacement.create(
this._placementHolder, this._dom, cellElem, {margins: getButtonMargins()}); this._placementHolder, this._dom, cellElem, {margins: getButtonMargins()});
// Reposition the editor if needed for external reasons (in practice, window resize). // Reposition the editor if needed for external reasons (in practice, window resize).
this.autoDispose(this._editorPlacement.onReposition.addListener(this._formulaEditor.resize, this._formulaEditor)); this.autoDispose(this._editorPlacement.onReposition.addListener(this._aceEditor.resize, this._aceEditor));
this._formulaEditor.onAttach(); this._aceEditor.onAttach();
this._formulaEditor.resize(); this._updateEditorPlaceholder();
this._aceEditor.resize();
this.focus(); this.focus();
} }
@ -292,33 +291,33 @@ export class FormulaEditor extends NewBaseEditor {
} }
public setFormula(formula: string) { public setFormula(formula: string) {
this._formulaEditor.setValue(formula); this._aceEditor.setValue(formula);
} }
public getCellValue() { public getCellValue() {
const value = this._formulaEditor.getValue(); const value = this._aceEditor.getValue();
// Strip the leading "=" sign, if any, in case users think it should start the formula body (as // Strip the leading "=" sign, if any, in case users think it should start the formula body (as
// it does in Excel, and because the equal sign is also used for formulas in Grist UI). // it does in Excel, and because the equal sign is also used for formulas in Grist UI).
return (value[0] === '=') ? value.slice(1) : value; return (value[0] === '=') ? value.slice(1) : value;
} }
public getTextValue() { public getTextValue() {
return this._formulaEditor.getValue(); return this._aceEditor.getValue();
} }
public getCursorPos() { public getCursorPos() {
const aceObj = this._formulaEditor.getEditor(); const aceObj = this._aceEditor.getEditor();
return aceObj.getSession().getDocument().positionToIndex(aceObj.getCursorPosition()); return aceObj.getSession().getDocument().positionToIndex(aceObj.getCursorPosition());
} }
public focus() { public focus() {
if (this.isDisposed()) { return; } if (this.isDisposed()) { return; }
this._formulaEditor.getEditor().focus(); this._aceEditor.getEditor().focus();
} }
public resize() { public resize() {
if (this.isDisposed()) { return; } if (this.isDisposed()) { return; }
this._formulaEditor.resize(); this._aceEditor.resize();
} }
public detach() { public detach() {
@ -331,25 +330,26 @@ export class FormulaEditor extends NewBaseEditor {
this._placementHolder.clear(); this._placementHolder.clear();
// We are going in the full formula edit mode right away. // We are going in the full formula edit mode right away.
this.options.editingFormula(true); this.options.editingFormula(true);
this._updateEditorPlaceholder();
// Set the focus in timeout, as the dom is added after this function. // Set the focus in timeout, as the dom is added after this function.
setTimeout(() => !this.isDisposed() && this._formulaEditor.resize(), 0); setTimeout(() => !this.isDisposed() && this._aceEditor.resize(), 0);
// Return the dom, it will be moved to the floating editor. // Return the dom, it will be moved to the floating editor.
return this._dom; return this._dom;
} }
private _updateEditorPlaceholder() { private _updateEditorPlaceholder() {
const editor = this._formulaEditor.getEditor(); const editor = this._aceEditor.getEditor();
const shouldShowPlaceholder = editor.session.getValue().length === 0; const shouldShowPlaceholder = editor.session.getValue().length === 0;
const placeholderNode = editor.renderer.emptyMessageNode; if (editor.renderer.emptyMessageNode) {
if (placeholderNode) {
// Remove the current placeholder if one is present. // Remove the current placeholder if one is present.
editor.renderer.scroller.removeChild(placeholderNode); editor.renderer.scroller.removeChild(editor.renderer.emptyMessageNode);
} }
if (!shouldShowPlaceholder) { if (!shouldShowPlaceholder) {
editor.renderer.emptyMessageNode = null; editor.renderer.emptyMessageNode = null;
} else { } else {
const withAiButton = this._canDetach && !this.isDetached.get() && HAS_FORMULA_ASSISTANT();
editor.renderer.emptyMessageNode = cssFormulaPlaceholder( editor.renderer.emptyMessageNode = cssFormulaPlaceholder(
!this._canDetach || this.isDetached.get() !withAiButton
? t('Enter formula.') ? t('Enter formula.')
: t('Enter formula or {{button}}.', { : t('Enter formula or {{button}}.', {
button: cssUseAssistantButton( button: cssUseAssistantButton(
@ -361,7 +361,6 @@ export class FormulaEditor extends NewBaseEditor {
); );
editor.renderer.scroller.appendChild(editor.renderer.emptyMessageNode); editor.renderer.scroller.appendChild(editor.renderer.emptyMessageNode);
} }
this._formulaEditor.resize();
} }
private _handleUseAssistantButtonClick(ev: MouseEvent) { private _handleUseAssistantButtonClick(ev: MouseEvent) {
@ -380,7 +379,7 @@ export class FormulaEditor extends NewBaseEditor {
}; };
} }
const placeholder: HTMLElement | undefined = this._formulaEditor.getEditor().renderer.emptyMessageNode; const placeholder: HTMLElement | undefined = this._aceEditor.getEditor().renderer.emptyMessageNode;
if (placeholder) { if (placeholder) {
// If we are showing the placeholder, fit it all on the same line. // If we are showing the placeholder, fit it all on the same line.
return this._editorPlacement.calcSizeWithPadding(elem, { return this._editorPlacement.calcSizeWithPadding(elem, {
@ -422,7 +421,7 @@ export class FormulaEditor extends NewBaseEditor {
const colId = col.origCol.peek().colId.peek(); const colId = col.origCol.peek().colId.peek();
const aceObj = this._formulaEditor.getEditor(); const aceObj = this._aceEditor.getEditor();
// Rect only to columns in the same table. // Rect only to columns in the same table.
if (col.tableId.peek() !== this.options.column.table.peek().tableId.peek()) { if (col.tableId.peek() !== this.options.column.table.peek().tableId.peek()) {
@ -451,7 +450,7 @@ export class FormulaEditor extends NewBaseEditor {
// Else touching a normal identifier, don't mangle it // Else touching a normal identifier, don't mangle it
} }
// Resize editor in case it is needed. // Resize editor in case it is needed.
this._formulaEditor.resize(); this._aceEditor.resize();
// This focus method will try to focus a textarea immediately and again on setTimeout. But // This focus method will try to focus a textarea immediately and again on setTimeout. But
// other things may happen by the setTimeout time, messing up focus. The reason the immediate // other things may happen by the setTimeout time, messing up focus. The reason the immediate

@ -74,7 +74,7 @@ export function makeGristConfig(options: MakeGristConfigOptons): GristLoadConfig
supportedLngs: readLoadedLngs(req?.i18n), supportedLngs: readLoadedLngs(req?.i18n),
namespaces: readLoadedNamespaces(req?.i18n), namespaces: readLoadedNamespaces(req?.i18n),
featureComments: isAffirmative(process.env.COMMENTS), featureComments: isAffirmative(process.env.COMMENTS),
featureFormulaAssistant: isAffirmative(process.env.GRIST_FORMULA_ASSISTANT), featureFormulaAssistant: Boolean(process.env.OPENAI_API_KEY),
supportEmail: SUPPORT_EMAIL, supportEmail: SUPPORT_EMAIL,
userLocale: (req as RequestWithLogin | undefined)?.user?.options?.locale, userLocale: (req as RequestWithLogin | undefined)?.user?.options?.locale,
telemetry: server?.getTelemetry().getTelemetryConfig(), telemetry: server?.getTelemetry().getTelemetryConfig(),

@ -3014,7 +3014,13 @@ export function withEnvironmentSnapshot(vars: Record<string, any>) {
// Test if the vars are already set, and if so, skip. // Test if the vars are already set, and if so, skip.
if (Object.keys(vars).every(k => process.env[k] === vars[k])) { return; } if (Object.keys(vars).every(k => process.env[k] === vars[k])) { return; }
oldEnv = new testUtils.EnvironmentSnapshot(); oldEnv = new testUtils.EnvironmentSnapshot();
Object.assign(process.env, vars); for(const key of Object.keys(vars)) {
if (vars[key] === undefined || vars[key] === null) {
delete process.env[key];
} else {
process.env[key] = vars[key];
}
}
await server.restart(); await server.restart();
}); });
after(async () => { after(async () => {

Loading…
Cancel
Save