(core) Improve dark mode

Summary:
Enhances dark mode support for the formula editor, and adds support to
the color select popup. Also fixes some bugs with dark mode.

Test Plan: Tested manually.

Reviewers: jarek

Reviewed By: jarek

Differential Revision: https://phab.getgrist.com/D3847
pull/495/head
George Gevoian 1 year ago
parent 9d0e6694fc
commit 8a0bb4d4fe

@ -1,10 +1,13 @@
import {setupAceEditorCompletions} from 'app/client/components/AceEditorCompletions';
import {colors} from 'app/client/ui2018/cssVars';
import {theme} from 'app/client/ui2018/cssVars';
import {Theme} from 'app/common/ThemePrefs';
import {getGristConfig} from 'app/common/urlUtils';
import * as ace from 'brace';
import {dom, DomArg, Observable, styled} from 'grainjs';
import {Computed, dom, DomArg, Listener, Observable, styled} from 'grainjs';
import debounce from 'lodash/debounce';
export interface ACLFormulaOptions {
gristTheme: Computed<Theme>;
initialValue: string;
readOnly: boolean;
placeholder: DomArg;
@ -19,7 +22,19 @@ export function aclFormulaEditor(options: ACLFormulaOptions) {
const editor: ace.Editor = ace.edit(editorElem);
// Set various editor options.
editor.setTheme('ace/theme/chrome');
function setAceTheme(gristTheme: Theme) {
const {enableCustomCss} = getGristConfig();
const gristAppearance = gristTheme.appearance;
const aceTheme = gristAppearance === 'dark' && !enableCustomCss ? 'dracula' : 'chrome';
editor.setTheme(`ace/theme/${aceTheme}`);
}
setAceTheme(options.gristTheme.get());
let themeListener: Listener | undefined;
if (!getGristConfig().enableCustomCss) {
themeListener = options.gristTheme.addListener((gristTheme) => {
setAceTheme(gristTheme);
});
}
// ACE editor resizes automatically when maxLines is set.
editor.setOptions({enableLiveAutocompletion: true, maxLines: 10});
editor.renderer.setShowGutter(false); // Default line numbers to hidden
@ -80,6 +95,7 @@ export function aclFormulaEditor(options: ACLFormulaOptions) {
}
return cssConditionInputAce(
dom.autoDispose(themeListener ?? null),
cssConditionInputAce.cls('-disabled', options.readOnly),
// ACE editor calls preventDefault on clicks into the scrollbar area, which prevents focus
// being set when the click happens to be into there. To ensure we can focus on such clicks
@ -100,22 +116,25 @@ const cssConditionInputAce = styled('div', `
cursor: pointer;
&:hover {
border: 1px solid ${colors.darkGrey};
border: 1px solid ${theme.accessRulesFormulaEditorBorderHover};
}
&:not(&-disabled):focus-within {
box-shadow: inset 0 0 0 1px ${colors.cursor};
border-color: ${colors.cursor};
box-shadow: inset 0 0 0 1px ${theme.accessRulesFormulaEditorFocus};
border-color: ${theme.accessRulesFormulaEditorFocus};
}
&:not(:focus-within) .ace_scroller, &-disabled .ace_scroller {
cursor: unset;
}
&-disabled, &-disabled:hover {
background-color: ${colors.mediumGreyOpaque};
background-color: ${theme.accessRulesFormulaEditorBgDisabled};
box-shadow: unset;
border-color: transparent;
}
&-disabled .ace-chrome {
background-color: ${colors.mediumGreyOpaque};
& .ace-chrome, & .ace-dracula {
background-color: ${theme.accessRulesFormulaEditorBg};
}
&-disabled .ace-chrome, &-disabled .ace-dracula {
background-color: ${theme.accessRulesFormulaEditorBgDisabled};
}
& .ace_marker-layer, & .ace_cursor-layer {
display: none;

@ -123,9 +123,9 @@ export class AccessRules extends Disposable {
// Map of tableId to basic metadata for all tables in the document.
private _aclResources = new Map<string, AclTableDescription>();
private _aclUsersPopup = ACLUsersPopup.create(this, this._gristDoc.docPageModel);
private _aclUsersPopup = ACLUsersPopup.create(this, this.gristDoc.docPageModel);
constructor(private _gristDoc: GristDoc) {
constructor(public gristDoc: GristDoc) {
super();
this._ruleStatus = Computed.create(this, (use) => {
const defRuleSet = use(this._docDefaultRuleSet);
@ -175,10 +175,10 @@ export class AccessRules extends Disposable {
// changes). Instead, react deliberately if rules change. Note that table/column renames would
// trigger changes to rules, so we don't need to listen for those separately.
for (const tableId of ['_grist_ACLResources', '_grist_ACLRules']) {
const tableData = this._gristDoc.docData.getTable(tableId)!;
const tableData = this.gristDoc.docData.getTable(tableId)!;
this.autoDispose(tableData.tableActionEmitter.addListener(this._onChange, this));
}
this.autoDispose(this._gristDoc.docPageModel.currentDoc.addListener(this._updateDocAccessData, this));
this.autoDispose(this.gristDoc.docPageModel.currentDoc.addListener(this._updateDocAccessData, this));
this.update().catch((e) => this._errorMessage.set(e.message));
}
@ -202,9 +202,9 @@ export class AccessRules extends Disposable {
const rules = this._ruleCollection;
const [ , , aclResources] = await Promise.all([
rules.update(this._gristDoc.docData, {log: console, pullOutSchemaEdit: true}),
rules.update(this.gristDoc.docData, {log: console, pullOutSchemaEdit: true}),
this._updateDocAccessData(),
this._gristDoc.docComm.getAclResources(),
this.gristDoc.docComm.getAclResources(),
]);
this._aclResources = new Map(Object.entries(aclResources.tables));
this._ruleProblems.set(aclResources.problems);
@ -244,7 +244,7 @@ export class AccessRules extends Disposable {
// Note that if anything has changed, we apply changes relative to the current state of the
// ACL tables (they may have changed by other users). So our changes will win.
const docData = this._gristDoc.docData;
const docData = this.gristDoc.docData;
const resourcesTable = docData.getMetaTable('_grist_ACLResources');
const rulesTable = docData.getMetaTable('_grist_ACLRules');
@ -346,7 +346,7 @@ export class AccessRules extends Disposable {
public buildDom() {
return cssOuter(
dom('div', this._gristDoc.behavioralPromptsManager.attachTip('accessRules', {
dom('div', this.gristDoc.behavioralPromptsManager.attachTip('accessRules', {
hideArrow: true,
})),
cssAddTableRow(
@ -485,7 +485,7 @@ export class AccessRules extends Disposable {
public async checkAclFormula(text: string): Promise<FormulaProperties> {
if (text) {
return this._gristDoc.docComm.checkAclFormula(text);
return this.gristDoc.docComm.checkAclFormula(text);
}
return {};
}
@ -1433,6 +1433,7 @@ class ObsUserAttributeRule extends Disposable {
cssColumnGroup(
cssCell1(
aclFormulaEditor({
gristTheme: this._accessRules.gristDoc.currentTheme,
initialValue: this._charId.get(),
readOnly: false,
setValue: (text) => this._setUserAttr(text),
@ -1655,6 +1656,7 @@ class ObsRulePart extends Disposable {
cssCell2(
wide ? cssCell4.cls('') : null,
aclFormulaEditor({
gristTheme: this._ruleSet.accessRules.gristDoc.currentTheme,
initialValue: this._aclFormula.get(),
readOnly: this.isBuiltIn(),
setValue: (value) => this._setAclFormula(value),

@ -3,17 +3,21 @@
}
.ace_grist_link {
color: var(--grist-color-light-green);
color: var(--grist-theme-ace-autocomplete-link, var(--grist-color-light-green));
text-decoration: underline;
cursor: pointer;
}
.ace_grist_example {
color: #8f8f8f;
color: var(--grist-theme-ace-autocomplete-secondary-fg);
}
.ace_editor.ace_autocomplete .ace_completion-highlight {
color: var(--grist-theme-ace-autocomplete-highlighted-fg, #000) !important;
}
.ace_editor.ace_autocomplete .ace_completion-highlight.ace_grist_link {
color: var(--grist-color-dark-green);
color: var(--grist-theme-ace-autocomplete-link-highlighted, var(--grist-color-dark-green)) !important;
}
.ace_editor.ace_autocomplete .ace_text-layer {
@ -22,6 +26,18 @@
}
.ace_editor.ace_autocomplete {
color: var(--grist-theme-ace-autocomplete-primary-fg) !important;
background: var(--grist-theme-ace-autocomplete-bg, #fbfbfb) !important;
border: 1px solid var(--grist-theme-ace-autocomplete-border, lightgray) !important;
width: 500px !important; /* the default in language_tools.js is 280px */
max-width: 80%; /* of the screen, for hypothetical mobile support */
}
.ace_editor.ace_autocomplete .ace_marker-layer .ace_line-hover {
background-color: var(--grist-theme-ace-autocomplete-line-bg-hover, rgba(233,233,253,0.4)) !important;
border: 1px solid var(--grist-theme-ace-autocomplete-line-border-hover, #abbffe) !important;
}
.ace_editor.ace_autocomplete .ace_marker-layer .ace_active-line {
background-color: var(--grist-theme-ace-autocomplete-active-line-bg, #CAD6FA) !important;
}

@ -204,7 +204,7 @@ AceEditor.prototype._setup = function() {
this.session = this.editor.getSession();
this.session.setMode('ace/mode/python');
const gristTheme = this.gristDoc?.docPageModel.appModel.currentTheme;
const gristTheme = this.gristDoc?.currentTheme;
this._setAceTheme(gristTheme?.get());
if (!getGristConfig().enableCustomCss && gristTheme) {
this.autoDispose(gristTheme.addListener((theme) => {

@ -229,7 +229,7 @@ export class ChartView extends Disposable {
this.listenTo(this.sortedRows, 'rowNotify', this._update);
this.autoDispose(this.sortedRows.getKoArray().subscribe(this._update));
this.autoDispose(this._formatterComp.subscribe(this._update));
this.autoDispose(this.gristDoc.docPageModel.appModel.currentTheme.addListener(() => this._update()));
this.autoDispose(this.gristDoc.currentTheme.addListener(() => this._update()));
}
public prepareToPrint(onOff: boolean) {

@ -50,3 +50,11 @@
.g-code-viewer .hljs-number {
color: var(--grist-theme-code-view-number, #880000);
}
.g-code-viewer .hljs-built_in {
color: var(--grist-theme-code-view-builtin, #397300);
}
.g-code-viewer .hljs-literal {
color: var(--grist-theme-code-view-literal, #78A960);
}

@ -182,6 +182,8 @@ export class GristDoc extends DisposableWithEvents {
// Holder for the popped up formula editor.
public readonly formulaPopup = Holder.create(this);
public readonly currentTheme = this.docPageModel.appModel.currentTheme;
private _actionLog: ActionLog;
private _undoStack: UndoStack;
private _lastOwnActionGroup: ActionGroupWithCursorPos|null = null;

@ -856,7 +856,8 @@ export class Importer extends DisposableWithEvents {
return formula;
};
return cssFieldFormula(use => formatFormula(use(column.formula)), {placeholder, maxLines: 1},
return cssFieldFormula(use => formatFormula(use(column.formula)),
{gristTheme: this._gristDoc.currentTheme, placeholder, maxLines: 1},
dom.cls('disabled'),
{tabIndex: '-1'},
dom.on('focus', (_ev, elem) => buildEditor(elem)),

@ -2,6 +2,7 @@ import {CustomView} from 'app/client/components/CustomView';
import {DataRowModel} from 'app/client/models/DataRowModel';
import DataTableModel from 'app/client/models/DataTableModel';
import {ViewSectionRec} from 'app/client/models/DocModel';
import {prefersDarkMode, prefersDarkModeObs} from 'app/client/ui2018/cssVars';
import {dom} from 'grainjs';
type RowId = number|'new';
@ -36,6 +37,16 @@ export async function printViewSection(layout: any, viewSection: ViewSectionRec)
}
function prepareToPrint(onOff: boolean) {
// window.print() is a blocking call, which means our listener for the
// `prefers-color-scheme: dark` media feature will not receive any updates for the
// duration that the print dialog is shown. This proves problematic since an event is
// sent just before the blocking call containing a value of false, regardless of the
// user agent's color scheme preference. It's not clear why this happens, but the result
// is Grist temporarily reverting to the light theme until the print dialog is dismissed.
// As a workaround, we'll temporarily pause our listener, and unpause after the print dialog
// is dismissed.
prefersDarkModeObs().pause();
// Hide all layout boxes that do NOT contain the section to be printed.
layout?.forEachBox((box: any) => {
if (!box.dom.contains(sectionElem)) {
@ -76,6 +87,10 @@ export async function printViewSection(layout: any, viewSection: ViewSectionRec)
prepareToPrint(false);
}
delete (window as any).afterPrintCallback;
prefersDarkModeObs().pause(false);
// This may have changed while window.print() was blocking.
prefersDarkModeObs().set(prefersDarkMode());
});
// Running print on a timeout makes it possible to test printing using selenium, and doesn't

@ -108,7 +108,7 @@ div:hover > .kf_tooltip {
box-shadow: 0 1px 1px 1px rgba(0,0,0,0.15);
line-height: 1.1rem;
font-size: 1rem;
color: #606060;
color: var(--grist-theme-prompt-fg, #606060);
z-index: 10;
}

@ -0,0 +1,41 @@
import {IDisposableOwner, Observable} from 'grainjs';
export interface PausableObservable<T> extends Observable<T> {
pause(shouldPause?: boolean): void;
}
/**
* Creates and returns an `Observable` that can be paused, effectively causing all
* calls to `set` to become noops until unpaused, at which point the last value
* passed to set, if any, will be applied.
*
* NOTE: It's only advisable to use this when there are no other alternatives; pausing
* updates and notifications to subscribers increases the chances of introducing bugs.
*/
export function createPausableObs<T>(
owner: IDisposableOwner|null,
value: T,
): PausableObservable<T> {
let _isPaused = false;
let _lastValue: T | undefined = undefined;
const obs = Observable.create<T>(owner, value);
const set = Symbol('set');
return Object.assign(obs, {
pause(shouldPause: boolean = true) {
_isPaused = shouldPause;
if (shouldPause) {
_lastValue = undefined;
} else if (_lastValue) {
obs.set(_lastValue);
_lastValue = undefined;
}
},
[set]: obs.set,
set(val: T) {
_lastValue = val;
if (_isPaused) { return; }
this[set](val);
}
});
}

@ -20,18 +20,24 @@ function makePrefFunctions<P extends keyof PrefsTypes>(prefsTypeName: P) {
*/
function getPrefsObs(appModel: AppModel): Observable<PrefsType> {
if (appModel.currentValidUser) {
const prefsObs = Observable.create<PrefsType>(null, appModel.currentOrg?.[prefsTypeName] ?? {});
let prefs: PrefsType | undefined;
if (prefsTypeName === 'userPrefs') {
prefs = appModel.currentValidUser.prefs;
} else {
prefs = appModel.currentOrg?.[prefsTypeName];
}
const prefsObs = Observable.create<PrefsType>(null, prefs ?? {});
return Computed.create(null, (use) => use(prefsObs))
.onWrite(prefs => {
prefsObs.set(prefs);
return appModel.api.updateOrg('current', {[prefsTypeName]: prefs});
.onWrite(newPrefs => {
prefsObs.set(newPrefs);
return appModel.api.updateOrg('current', {[prefsTypeName]: newPrefs});
});
} else {
const userId = appModel.currentUser?.id || 0;
const jsonPrefsObs = localStorageObs(`${prefsTypeName}:u=${userId}`);
return Computed.create(null, jsonPrefsObs, (use, p) => (p && JSON.parse(p) || {}) as PrefsType)
.onWrite(prefs => {
jsonPrefsObs.set(JSON.stringify(prefs));
.onWrite(newPrefs => {
jsonPrefsObs.set(JSON.stringify(newPrefs));
});
}
}

@ -1,13 +1,17 @@
import {colors, theme, vars} from 'app/client/ui2018/cssVars';
import {theme, vars} from 'app/client/ui2018/cssVars';
import {Theme} from 'app/common/ThemePrefs';
import {getGristConfig} from 'app/common/urlUtils';
import * as ace from 'brace';
import {BindableValue, dom, DomElementArg, styled, subscribeElem} from 'grainjs';
import {BindableValue, Computed, dom, DomElementArg, Observable, styled, subscribeElem} from 'grainjs';
// tslint:disable:no-var-requires
require('brace/ext/static_highlight');
require("brace/mode/python");
require("brace/theme/chrome");
require('brace/theme/dracula');
export interface ICodeOptions {
gristTheme: Computed<Theme>;
placeholder?: string;
maxLines?: number;
}
@ -15,29 +19,48 @@ export interface ICodeOptions {
export function buildHighlightedCode(
code: BindableValue<string>, options: ICodeOptions, ...args: DomElementArg[]
): HTMLElement {
const {gristTheme, placeholder, maxLines} = options;
const {enableCustomCss} = getGristConfig();
const highlighter = ace.acequire('ace/ext/static_highlight');
const PythonMode = ace.acequire('ace/mode/python').Mode;
const aceTheme = ace.acequire('ace/theme/chrome');
const chrome = ace.acequire('ace/theme/chrome');
const dracula = ace.acequire('ace/theme/dracula');
const mode = new PythonMode();
return cssHighlightedCode(
dom('div',
elem => subscribeElem(elem, code, (codeText) => {
if (codeText) {
if (options.maxLines) {
// If requested, trim to maxLines, and add an ellipsis at the end.
// (Long lines are also truncated with an ellpsis via text-overflow style.)
const lines = codeText.split(/\n/);
if (lines.length > options.maxLines) {
codeText = lines.slice(0, options.maxLines).join("\n") + " \u2026"; // Ellipsis
}
}
elem.innerHTML = highlighter.render(codeText, mode, aceTheme, 1, true).html;
} else {
elem.textContent = options.placeholder || '';
const codeText = Observable.create(null, '');
const codeTheme = Observable.create(null, gristTheme.get());
function updateHighlightedCode(elem: HTMLElement) {
let text = codeText.get();
if (text) {
if (maxLines) {
// If requested, trim to maxLines, and add an ellipsis at the end.
// (Long lines are also truncated with an ellpsis via text-overflow style.)
const lines = text.split(/\n/);
if (lines.length > maxLines) {
text = lines.slice(0, maxLines).join("\n") + " \u2026"; // Ellipsis
}
}
const aceTheme = codeTheme.get().appearance === 'dark' && !enableCustomCss ? dracula : chrome;
elem.innerHTML = highlighter.render(text, mode, aceTheme, 1, true).html;
} else {
elem.textContent = placeholder || '';
}
}
return cssHighlightedCode(
dom.autoDispose(codeText),
dom.autoDispose(codeTheme),
elem => subscribeElem(elem, code, (newCodeText) => {
codeText.set(newCodeText);
updateHighlightedCode(elem);
}),
),
elem => subscribeElem(elem, gristTheme, (newCodeTheme) => {
codeTheme.set(newCodeTheme);
updateHighlightedCode(elem);
}),
...args,
);
}
@ -46,9 +69,9 @@ export function buildHighlightedCode(
export const cssCodeBlock = styled('div', `
font-family: 'Monaco', 'Menlo', monospace;
font-size: ${vars.smallFontSize};
background-color: ${colors.light};
background-color: ${theme.highlightedCodeBlockBg};
&[disabled], &.disabled {
background-color: ${colors.mediumGreyOpaque};
background-color: ${theme.highlightedCodeBlockBgDisabled};
}
`);
@ -57,14 +80,14 @@ const cssHighlightedCode = styled(cssCodeBlock, `
white-space: pre;
overflow: hidden;
text-overflow: ellipsis;
border: 1px solid ${colors.darkGrey};
border: 1px solid ${theme.highlightedCodeBorder};
border-radius: 3px;
min-height: 28px;
padding: 5px 6px;
color: ${colors.slate};
color: ${theme.highlightedCodeFg};
&.disabled, &.disabled .ace-chrome {
background-color: ${colors.mediumGreyOpaque};
&.disabled, &.disabled .ace-chrome, &.disabled .ace-dracula {
background-color: ${theme.highlightedCodeBgDisabled};
}
& .ace_line {
overflow: hidden;
@ -80,7 +103,7 @@ export const cssFieldFormula = styled(buildHighlightedCode, `
--icon-color: ${theme.accentIcon};
&-disabled-icon.formula_field_sidepane::before {
--icon-color: ${theme.lightText};
--icon-color: ${theme.iconDisabled};
}
&-disabled {
pointer-events: none;

@ -7,7 +7,7 @@ import {reportError} from 'app/client/models/errors';
import {cssHelp, cssLabel, cssRow, cssSeparator} from 'app/client/ui/RightPanelStyles';
import {cssDragRow, cssFieldEntry, cssFieldLabel} from 'app/client/ui/VisibleFieldsConfig';
import {basicButton, primaryButton, textButton} from 'app/client/ui2018/buttons';
import {colors, vars} from 'app/client/ui2018/cssVars';
import {theme, vars} from 'app/client/ui2018/cssVars';
import {cssDragger} from 'app/client/ui2018/draggableList';
import {textInput} from 'app/client/ui2018/editableLabel';
import {IconName} from 'app/client/ui2018/IconList';
@ -511,7 +511,7 @@ export class CustomSectionConfig extends Disposable {
const cssWarningWrapper = styled('div', `
padding-left: 8px;
padding-top: 6px;
--icon-color: ${colors.error}
--icon-color: ${theme.iconError}
`);
const cssColumns = styled('div', `
@ -534,7 +534,7 @@ const cssSection = styled('div', `
const cssMenu = styled('div', `
& > li:first-child {
border-bottom: 1px solid ${colors.mediumGrey};
border-bottom: 1px solid ${theme.menuBorder};
}
`);
@ -556,30 +556,37 @@ const cssRemoveIcon = styled(icon, `
const cssSubLabel = styled('span', `
text-transform: none;
font-size: ${vars.xsmallFontSize};
color: ${colors.slate};
color: ${theme.lightText};
`);
const cssAddMapping = styled('div', `
display: flex;
cursor: pointer;
color: ${colors.lightGreen};
--icon-color: ${colors.lightGreen};
color: ${theme.controlFg};
--icon-color: ${theme.controlFg};
&:not(:first-child) {
margin-top: 8px;
}
&:hover, &:focus, &:active {
color: ${colors.darkGreen};
--icon-color: ${colors.darkGreen};
color: ${theme.controlHoverFg};
--icon-color: ${theme.controlHoverFg};
}
`);
const cssTextInput = styled(textInput, `
flex: 1 0 auto;
color: ${theme.inputFg};
background-color: ${theme.inputBg};
&:disabled {
color: ${colors.slate};
background-color: ${colors.lightGrey};
color: ${theme.inputDisabledFg};
background-color: ${theme.inputDisabledBg};
pointer-events: none;
}
&::placeholder {
color: ${theme.inputPlaceholderFg};
}
`);

@ -17,6 +17,7 @@ import {GRIST_FORMULA_ASSISTANT} from 'app/client/models/features';
import {selectMenu, selectOption, selectTitle} from 'app/client/ui2018/menus';
import {createFormulaErrorObs, cssError} from 'app/client/widgets/FormulaEditor';
import {sanitizeIdent} from 'app/common/gutil';
import {Theme} from 'app/common/ThemePrefs';
import {bundleChanges, Computed, dom, DomContents, DomElementArg, fromKo, MultiHolder,
Observable, styled} from 'grainjs';
import * as ko from 'knockout';
@ -316,10 +317,13 @@ export function buildFormulaConfig(
cssRow(formulaField = buildFormula(
origColumn,
buildEditor,
t("Enter formula"),
disableOtherActions,
onSave,
clearState)),
{
gristTheme: gristDoc.currentTheme,
placeholder: t("Enter formula"),
disabled: disableOtherActions,
onSave,
onCancel: clearState,
})),
dom.maybe(errorMessage, errMsg => cssRow(cssError(errMsg), testId('field-error-count'))),
];
@ -404,14 +408,21 @@ export function buildFormulaConfig(
]);
}
interface BuildFormulaOptions {
gristTheme: Computed<Theme>;
placeholder: string;
disabled: Observable<boolean>;
onSave?: SaveHandler;
onCancel?: () => void;
}
function buildFormula(
column: ColumnRec,
buildEditor: BuildEditor,
placeholder: string,
disabled: Observable<boolean>,
onSave?: SaveHandler,
onCancel?: () => void) {
return cssFieldFormula(column.formula, {placeholder, maxLines: 2},
column: ColumnRec,
buildEditor: BuildEditor,
options: BuildFormulaOptions
) {
const {gristTheme, placeholder, disabled, onSave, onCancel} = options;
return cssFieldFormula(column.formula, {gristTheme, placeholder, maxLines: 2},
dom.cls('formula_field_sidepane'),
cssFieldFormula.cls('-disabled', disabled),
cssFieldFormula.cls('-disabled-icon', use => !use(column.formula)),

@ -236,7 +236,10 @@ function buildChat(owner: Disposable, context: Context & { formulaClicked: (form
} else {
return cssAiMessage(
cssAvatar(cssAiImage()),
buildHighlightedCode(entry.message, { maxLines: 10 }, cssCodeStyles.cls('')),
buildHighlightedCode(entry.message, {
gristTheme: grist.currentTheme,
maxLines: 10,
}, cssCodeStyles.cls('')),
cssCopyIconWrapper(
icon('Copy', dom.on('click', () => context.formulaClicked(entry.message))),
)

@ -1,6 +1,6 @@
import {basicButton, primaryButton} from 'app/client/ui2018/buttons';
import {isLight, swatches} from 'app/client/ui2018/ColorPalette';
import {colors, testId, theme, vars} from 'app/client/ui2018/cssVars';
import {testId, theme, vars} from 'app/client/ui2018/cssVars';
import {textInput} from 'app/client/ui2018/editableLabel';
import {IconName} from 'app/client/ui2018/IconList';
import {icon} from 'app/client/ui2018/icons';
@ -366,24 +366,24 @@ class FontComponent extends Disposable {
const cssFontOptions = styled('div', `
display: flex;
gap: 1px;
background: ${colors.darkGrey};
border: 1px solid ${colors.darkGrey};
background: ${theme.colorSelectFontOptionsBorder};
border: 1px solid ${theme.colorSelectFontOptionsBorder};
`);
const cssFontOption = styled('div', `
display: grid;
place-items: center;
flex-grow: 1;
background: ${colors.light};
--icon-color: ${colors.dark};
background: ${theme.colorSelectFontOptionBg};
--icon-color: ${theme.colorSelectFontOptionFg};
height: 24px;
cursor: pointer;
&:hover:not(&-selected) {
background: ${colors.lightGrey};
background: ${theme.colorSelectFontOptionBgHover};
}
&-selected {
background: ${colors.dark};
--icon-color: ${colors.light}
background: ${theme.colorSelectFontOptionBgSelected};
--icon-color: ${theme.colorSelectFontOptionFgSelected}
}
`);
@ -409,6 +409,7 @@ const cssControlRow = styled('div', `
`);
const cssHeaderRow = styled('div', `
color: ${theme.colorSelectFg};
text-transform: uppercase;
font-size: ${vars.smallFontSize};
margin-bottom: 12px;
@ -430,8 +431,8 @@ const cssVSpacer = styled('div', `
const cssContainer = styled('div', `
padding: 18px 16px;
background-color: white;
box-shadow: 0 2px 16px 0 rgba(38,38,51,0.6);
background-color: ${theme.colorSelectBg};
box-shadow: 0 2px 16px 0 ${theme.colorSelectShadow};
z-index: 20;
margin: 2px 0;
&:focus {
@ -445,13 +446,13 @@ const cssContent = styled('div', `
`);
const cssHexBox = styled(textInput, `
border: 1px solid ${theme.inputBorder};
border: 1px solid ${theme.colorSelectInputBorder};
border-left: none;
font-size: ${vars.smallFontSize};
display: flex;
align-items: center;
color: ${theme.lightText};
background-color: ${theme.inputBg};
color: ${theme.colorSelectInputFg};
background-color: ${theme.colorSelectInputBg};
width: 56px;
outline: none;
padding: 0 3px;
@ -460,7 +461,7 @@ const cssHexBox = styled(textInput, `
`);
const cssLightBorder = styled('div', `
border: 1px solid #D9D9D9;
border: 1px solid ${theme.colorSelectColorSquareBorder};
`);
const cssColorSquare = styled('div', `
@ -471,16 +472,16 @@ const cssColorSquare = styled('div', `
align-items: center;
position: relative;
&-selected {
outline: 1px solid #D9D9D9;
outline: 1px solid ${theme.colorSelectColorSquareBorder};
outline-offset: 1px;
}
`);
const cssEmptyBox = styled(cssColorSquare, `
--icon-color: ${colors.error};
--icon-color: ${theme.iconError};
border: 1px solid #D9D9D9;
&-selected {
outline: 1px solid ${colors.dark};
outline: 1px solid ${theme.colorSelectColorSquareBorderEmpty};
outline-offset: 1px;
}
`);
@ -493,7 +494,7 @@ const cssFontIcon = styled(icon, `
const cssNoneIcon = styled(icon, `
height: 100%;
width: 100%;
--icon-color: ${colors.error}
--icon-color: ${theme.iconError}
`);
const cssButtonIcon = styled(cssColorSquare, `

@ -6,6 +6,7 @@
* https://css-tricks.com/snippets/css/system-font-stack/
*
*/
import {createPausableObs, PausableObservable} from 'app/client/lib/pausableObs';
import {getStorage} from 'app/client/lib/storage';
import {urlState} from 'app/client/models/gristUrlState';
import {getTheme, ProductFlavor} from 'app/client/ui/CustomThemes';
@ -257,6 +258,9 @@ export const theme = {
'rgba(76, 86, 103, 0.24)'),
popupCloseButtonFg: new CustomProp('theme-popup-close-button-fg', undefined, colors.slate),
/* Prompts */
promptFg: new CustomProp('theme-prompt-fg', undefined, '#606060'),
/* Progress Bars */
progressBarFg: new CustomProp('theme-progress-bar-fg', undefined, colors.lightGreen),
progressBarErrorFg: new CustomProp('theme-progress-bar-error-fg', undefined, colors.error),
@ -382,6 +386,10 @@ export const theme = {
filterBarButtonSavedHoverBg: new CustomProp('theme-filter-bar-button-saved-hover-bg', undefined,
colors.darkGrey),
/* Icons */
iconDisabled: new CustomProp('theme-icon-disabled', undefined, colors.slate),
iconError: new CustomProp('theme-icon-error', undefined, colors.error),
/* Icon Buttons */
iconButtonFg: new CustomProp('theme-icon-button-fg', undefined, colors.light),
iconButtonPrimaryBg: new CustomProp('theme-icon-button-primary-bg', undefined,
@ -589,6 +597,8 @@ export const theme = {
codeViewParams: new CustomProp('theme-code-view-params', undefined, '#444'),
codeViewString: new CustomProp('theme-code-view-string', undefined, '#880000'),
codeViewNumber: new CustomProp('theme-code-view-number', undefined, '#880000'),
codeViewBuiltin: new CustomProp('theme-code-view-builtin', undefined, '#397300'),
codeViewLiteral: new CustomProp('theme-code-view-literal', undefined, '#78A960'),
/* Importer */
importerTableInfoBorder: new CustomProp('theme-importer-table-info-border', undefined, colors.darkGrey),
@ -646,6 +656,14 @@ export const theme = {
undefined, colors.light),
accessRulesColumnItemIconHoverBg: new CustomProp('theme-access-rules-column-item-icon-hover-bg',
undefined, colors.slate),
accessRulesFormulaEditorBg: new CustomProp('theme-access-rules-formula-editor-bg', undefined,
'white'),
accessRulesFormulaEditorBorderHover: new CustomProp(
'theme-access-rules-formula-editor-border-hover', undefined, colors.darkGrey),
accessRulesFormulaEditorBgDisabled: new CustomProp(
'theme-access-rules-formula-editor-bg-disabled', undefined, colors.mediumGreyOpaque),
accessRulesFormulaEditorFocus: new CustomProp('theme-access-rules-formula-editor-focus',
undefined, colors.cursor),
/* Cells */
cellFg: new CustomProp('theme-cell-fg', undefined, 'unset'),
@ -701,6 +719,63 @@ export const theme = {
tutorialsPopupHeaderFg: new CustomProp('theme-tutorials-popup-header-fg', undefined,
colors.lightGreen),
tutorialsPopupBoxBg: new CustomProp('theme-tutorials-popup-box-bg', undefined, '#F5F5F5'),
/* Ace Autocomplete */
aceAutocompletePrimaryFg: new CustomProp('theme-ace-autocomplete-primary-fg', undefined, '#444'),
aceAutocompleteSecondaryFg: new CustomProp('theme-ace-autocomplete-secondary-fg', undefined,
'#8f8f8f'),
aceAutocompleteHighlightedFg: new CustomProp('theme-ace-autocomplete-highlighted-fg', undefined, '#000'),
aceAutocompleteBg: new CustomProp('theme-ace-autocomplete-bg', undefined, '#FBFBFB'),
aceAutocompleteBorder: new CustomProp('theme-ace-autocomplete-border', undefined, 'lightgray'),
aceAutocompleteLink: new CustomProp('theme-ace-autocomplete-link', undefined, colors.lightGreen),
aceAutocompleteLinkHighlighted: new CustomProp('theme-ace-autocomplete-link-highlighted',
undefined, colors.darkGreen),
aceAutocompleteActiveLineBg: new CustomProp('theme-ace-autocomplete-active-line-bg',
undefined, '#CAD6FA'),
aceAutocompleteLineBorderHover: new CustomProp('theme-ace-autocomplete-line-border-hover',
undefined, '#abbffe'),
aceAutocompleteLineBgHover: new CustomProp('theme-ace-autocomplete-line-bg-hover',
undefined, 'rgba(233,233,253,0.4)'),
/* Color Select */
colorSelectFg: new CustomProp('theme-color-select-fg', undefined, colors.dark),
colorSelectBg: new CustomProp('theme-color-select-bg', undefined, 'white'),
colorSelectShadow: new CustomProp('theme-color-select-shadow', undefined,
'rgba(38,38,51,0.6)'),
colorSelectFontOptionsBorder: new CustomProp('theme-color-select-font-options-border',
undefined, colors.darkGrey),
colorSelectFontOptionFg: new CustomProp('theme-color-select-font-option-fg',
undefined, colors.dark),
colorSelectFontOptionBg: new CustomProp('theme-color-select-font-option-bg',
undefined, colors.light),
colorSelectFontOptionBgHover: new CustomProp('theme-color-select-font-option-bg-hover',
undefined, colors.lightGrey),
colorSelectFontOptionFgSelected: new CustomProp('theme-color-select-font-option-selected-fg',
undefined, colors.light),
colorSelectFontOptionBgSelected: new CustomProp('theme-color-select-font-option-selected-bg',
undefined, colors.dark),
colorSelectColorSquareBorder: new CustomProp('theme-color-select-color-square-border',
undefined, '#D9D9D9'),
colorSelectColorSquareBorderEmpty: new CustomProp('theme-color-select-color-square-border-empty',
undefined, colors.dark),
colorSelectInputFg: new CustomProp('theme-color-select-input-fg',
undefined, colors.slate),
colorSelectInputBg: new CustomProp('theme-color-select-input-bg',
undefined, 'white'),
colorSelectInputBorder: new CustomProp('theme-color-select-input-border',
undefined, colors.darkGrey),
/* Highlighted Code */
highlightedCodeBlockBg: new CustomProp('theme-highlighted-code-block-bg', undefined,
colors.light),
highlightedCodeBlockBgDisabled: new CustomProp('theme-highlighted-code-block-bg-disabled',
undefined, colors.mediumGreyOpaque),
highlightedCodeFg: new CustomProp('theme-highlighted-code-fg',
undefined, colors.slate),
highlightedCodeBorder: new CustomProp('theme-highlighted-code-border',
undefined, colors.darkGrey),
highlightedCodeBgDisabled: new CustomProp('theme-highlighted-code-bg-disabled',
undefined, colors.mediumGreyOpaque),
};
const cssColors = values(colors).map(v => v.decl()).join('\n');
@ -823,15 +898,19 @@ export function isScreenResizing(): Observable<boolean> {
return _isScreenResizingObs;
}
let _prefersDarkModeObs: Observable<boolean>|undefined;
export function prefersDarkMode(): boolean {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
let _prefersDarkModeObs: PausableObservable<boolean>|undefined;
/**
* Returns a singleton observable for whether the user agent prefers dark mode.
*/
export function prefersDarkModeObs(): Observable<boolean> {
export function prefersDarkModeObs(): PausableObservable<boolean> {
if (!_prefersDarkModeObs) {
const query = window.matchMedia('(prefers-color-scheme: dark)');
const obs = Observable.create<boolean>(null, query.matches);
const obs = createPausableObs<boolean>(null, query.matches);
query.addEventListener('change', event => obs.set(event.matches));
_prefersDarkModeObs = obs;
}

@ -186,7 +186,7 @@ export class ConditionalStyle extends Disposable {
) {
return cssFieldFormula(
formula,
{ maxLines: 1 },
{ gristTheme: this._gristDoc.currentTheme, maxLines: 1 },
dom.cls('formula_field_sidepane'),
dom.cls(cssErrorBorder.className, hasError),
{ tabIndex: '-1' },

@ -1,3 +1,5 @@
import {UserPrefs} from 'app/common/Prefs';
// User profile info for the user. When using Cognito, it is fetched during login.
export interface UserProfile {
email: string;
@ -16,6 +18,7 @@ export interface FullUser extends UserProfile {
ref?: string|null; // Not filled for anonymous users.
allowGoogleLogin?: boolean; // when present, specifies whether logging in via Google is possible.
isSupport?: boolean; // set if user is a special support user.
prefs?: UserPrefs;
}
export interface LoginSessionAPI {

@ -93,6 +93,7 @@ export const ThemeColors = t.iface([], {
"popup-shadow-inner": "string",
"popup-shadow-outer": "string",
"popup-close-button-fg": "string",
"prompt-fg": "string",
"progress-bar-fg": "string",
"progress-bar-error-fg": "string",
"progress-bar-bg": "string",
@ -167,6 +168,8 @@ export const ThemeColors = t.iface([], {
"filter-bar-button-saved-fg": "string",
"filter-bar-button-saved-bg": "string",
"filter-bar-button-saved-hover-bg": "string",
"icon-disabled": "string",
"icon-error": "string",
"icon-button-fg": "string",
"icon-button-primary-bg": "string",
"icon-button-primary-hover-bg": "string",
@ -287,6 +290,8 @@ export const ThemeColors = t.iface([], {
"code-view-params": "string",
"code-view-string": "string",
"code-view-number": "string",
"code-view-builtin": "string",
"code-view-literal": "string",
"importer-table-info-border": "string",
"importer-preview-border": "string",
"importer-skipped-table-overlay": "string",
@ -319,6 +324,10 @@ export const ThemeColors = t.iface([], {
"access-rules-column-item-icon-fg": "string",
"access-rules-column-item-icon-hover-fg": "string",
"access-rules-column-item-icon-hover-bg": "string",
"access-rules-formula-editor-bg": "string",
"access-rules-formula-editor-border-hover": "string",
"access-rules-formula-editor-bg-disabled": "string",
"access-rules-formula-editor-focus": "string",
"cell-fg": "string",
"cell-bg": "string",
"cell-zebra-bg": "string",
@ -348,6 +357,35 @@ export const ThemeColors = t.iface([], {
"tutorials-popup-border": "string",
"tutorials-popup-header-fg": "string",
"tutorials-popup-box-bg": "string",
"ace-autocomplete-primary-fg": "string",
"ace-autocomplete-secondary-fg": "string",
"ace-autocomplete-highlighted-fg": "string",
"ace-autocomplete-bg": "string",
"ace-autocomplete-border": "string",
"ace-autocomplete-link": "string",
"ace-autocomplete-link-highlighted": "string",
"ace-autocomplete-active-line-bg": "string",
"ace-autocomplete-line-border-hover": "string",
"ace-autocomplete-line-bg-hover": "string",
"color-select-fg": "string",
"color-select-bg": "string",
"color-select-shadow": "string",
"color-select-font-options-border": "string",
"color-select-font-option-fg": "string",
"color-select-font-option-bg": "string",
"color-select-font-option-bg-hover": "string",
"color-select-font-option-fg-selected": "string",
"color-select-font-option-bg-selected": "string",
"color-select-color-square-border": "string",
"color-select-color-square-border-empty": "string",
"color-select-input-fg": "string",
"color-select-input-bg": "string",
"color-select-input-border": "string",
"highlighted-code-block-bg": "string",
"highlighted-code-block-bg-disabled": "string",
"highlighted-code-fg": "string",
"highlighted-code-border": "string",
"highlighted-code-bg-disabled": "string",
});
const exportedTypeSuite: t.ITypeSuite = {

@ -110,6 +110,9 @@ export interface ThemeColors {
'popup-shadow-outer': string;
'popup-close-button-fg': string;
/* Prompts */
'prompt-fg': string;
/* Progress Bars */
'progress-bar-fg': string;
'progress-bar-error-fg': string;
@ -216,6 +219,10 @@ export interface ThemeColors {
'filter-bar-button-saved-bg': string;
'filter-bar-button-saved-hover-bg': string;
/* Icons */
'icon-disabled': string;
'icon-error': string;
/* Icon Buttons */
'icon-button-fg': string;
'icon-button-primary-bg': string;
@ -373,6 +380,8 @@ export interface ThemeColors {
'code-view-params': string;
'code-view-string': string;
'code-view-number': string;
'code-view-builtin': string;
'code-view-literal': string;
/* Importer */
'importer-table-info-border': string;
@ -415,6 +424,10 @@ export interface ThemeColors {
'access-rules-column-item-icon-fg': string;
'access-rules-column-item-icon-hover-fg': string;
'access-rules-column-item-icon-hover-bg': string;
'access-rules-formula-editor-bg': string;
'access-rules-formula-editor-border-hover': string;
'access-rules-formula-editor-bg-disabled': string;
'access-rules-formula-editor-focus': string;
/* Cells */
'cell-fg': string;
@ -456,6 +469,41 @@ export interface ThemeColors {
'tutorials-popup-border': string;
'tutorials-popup-header-fg': string;
'tutorials-popup-box-bg': string;
/* Ace Autocomplete */
'ace-autocomplete-primary-fg': string;
'ace-autocomplete-secondary-fg': string;
'ace-autocomplete-highlighted-fg': string;
'ace-autocomplete-bg': string;
'ace-autocomplete-border': string;
'ace-autocomplete-link': string;
'ace-autocomplete-link-highlighted': string;
'ace-autocomplete-active-line-bg': string;
'ace-autocomplete-line-border-hover': string;
'ace-autocomplete-line-bg-hover': string;
/* Color Select */
'color-select-fg': string;
'color-select-bg': string;
'color-select-shadow': string;
'color-select-font-options-border': string;
'color-select-font-option-fg': string;
'color-select-font-option-bg': string;
'color-select-font-option-bg-hover': string;
'color-select-font-option-fg-selected': string;
'color-select-font-option-bg-selected': string;
'color-select-color-square-border': string;
'color-select-color-square-border-empty': string;
'color-select-input-fg': string;
'color-select-input-bg': string;
'color-select-input-border': string;
/* Highlighted Code */
'highlighted-code-block-bg': string;
'highlighted-code-block-bg-disabled': string;
'highlighted-code-fg': string;
'highlighted-code-border': string;
'highlighted-code-bg-disabled': string;
}
export const ThemePrefsChecker = createCheckers(ThemePrefsTI).ThemePrefs as CheckerT<ThemePrefs>;

@ -89,6 +89,9 @@ export const GristDark: ThemeColors = {
'popup-shadow-outer': '#000000',
'popup-close-button-fg': '#A4A4A4',
/* Prompts */
'prompt-fg': '#A4A4A4',
/* Progress Bars */
'progress-bar-fg': '#1DA270',
'progress-bar-error-fg': '#FF6666',
@ -195,6 +198,10 @@ export const GristDark: ThemeColors = {
'filter-bar-button-saved-bg': '#555563',
'filter-bar-button-saved-hover-bg': '#69697D',
/* Icons */
'icon-disabled': '#A4A4A4',
'icon-error': '#FFA500',
/* Icon Buttons */
'icon-button-fg': '#FFFFFF',
'icon-button-primary-bg': '#1DA270',
@ -352,6 +359,8 @@ export const GristDark: ThemeColors = {
'code-view-params': '#D2D2D2',
'code-view-string': '#ED7373',
'code-view-number': '#ED7373',
'code-view-builtin': '#BFE6D8',
'code-view-literal': '#9ED682',
/* Importer */
'importer-table-info-border': '#69697D',
@ -394,6 +403,10 @@ export const GristDark: ThemeColors = {
'access-rules-column-item-icon-fg': '#A4A4A4',
'access-rules-column-item-icon-hover-fg': '#EFEFEF',
'access-rules-column-item-icon-hover-bg': '#A4A4A4',
'access-rules-formula-editor-bg': '#32323F',
'access-rules-formula-editor-border-hover': '#69697D',
'access-rules-formula-editor-bg-disabled': '#57575F',
'access-rules-formula-editor-focus': '#1DA270',
/* Cells */
'cell-fg': '#FFFFFF',
@ -435,4 +448,39 @@ export const GristDark: ThemeColors = {
'tutorials-popup-border': '#69697D',
'tutorials-popup-header-fg': '#FFFFFF',
'tutorials-popup-box-bg': '#57575F',
/* Ace Autocomplete */
'ace-autocomplete-primary-fg': '#EFEFEF',
'ace-autocomplete-secondary-fg': '#A4A4A4',
'ace-autocomplete-highlighted-fg': '#FFFFFF',
'ace-autocomplete-bg': '#32323F',
'ace-autocomplete-border': '#69697D',
'ace-autocomplete-link': '#28BE86',
'ace-autocomplete-link-highlighted': '#45D48B',
'ace-autocomplete-active-line-bg': '#555563',
'ace-autocomplete-line-border-hover': 'rgba(111,111,117,0.3)',
'ace-autocomplete-line-bg-hover': 'rgba(111,111,117,0.3)',
/* Color Select */
'color-select-fg': '#EFEFEF',
'color-select-bg': '#32323F',
'color-select-shadow': '#000000',
'color-select-font-options-border': '#69697D',
'color-select-font-option-fg': '#EFEFEF',
'color-select-font-option-bg': '#32323F',
'color-select-font-option-bg-hover': '#262633',
'color-select-font-option-fg-selected': '#EFEFEF',
'color-select-font-option-bg-selected': '#555563',
'color-select-color-square-border': '#A4A4A4',
'color-select-color-square-border-empty': '#EFEFEF',
'color-select-input-fg': '#EFEFEF',
'color-select-input-bg': '#32323F',
'color-select-input-border': '#69697D',
/* Highlighted Code */
'highlighted-code-block-bg': '#262633',
'highlighted-code-block-bg-disabled': '#555563',
'highlighted-code-fg': '#A4A4A4',
'highlighted-code-border': '#69697D',
'highlighted-code-bg-disabled': '#555563',
};

@ -89,6 +89,9 @@ export const GristLight: ThemeColors = {
'popup-shadow-outer': 'rgba(76, 86, 103, 0.24)',
'popup-close-button-fg': '#929299',
/* Prompts */
'prompt-fg': '#606060',
/* Progress Bars */
'progress-bar-fg': '#16B378',
'progress-bar-error-fg': '#D0021B',
@ -195,6 +198,10 @@ export const GristLight: ThemeColors = {
'filter-bar-button-saved-bg': '#929299',
'filter-bar-button-saved-hover-bg': '#D9D9D9',
/* Icons */
'icon-disabled': '#929299',
'icon-error': '#D0021B',
/* Icon Buttons */
'icon-button-fg': '#FFFFFF',
'icon-button-primary-bg': '#16B378',
@ -352,6 +359,8 @@ export const GristLight: ThemeColors = {
'code-view-params': '#444',
'code-view-string': '#880000',
'code-view-number': '#880000',
'code-view-builtin': '#397300',
'code-view-literal': '#78A960',
/* Importer */
'importer-table-info-border': '#D9D9D9',
@ -394,6 +403,10 @@ export const GristLight: ThemeColors = {
'access-rules-column-item-icon-fg': '#929299',
'access-rules-column-item-icon-hover-fg': '#FFFFFF',
'access-rules-column-item-icon-hover-bg': '#929299',
'access-rules-formula-editor-bg': 'white',
'access-rules-formula-editor-border-hover': '#D9D9D9',
'access-rules-formula-editor-bg-disabled': '#E8E8E8',
'access-rules-formula-editor-focus': '#16B378',
/* Cells */
'cell-fg': 'black',
@ -435,4 +448,39 @@ export const GristLight: ThemeColors = {
'tutorials-popup-border': '#D9D9D9',
'tutorials-popup-header-fg': '#FFFFFF',
'tutorials-popup-box-bg': '#F5F5F5',
/* Ace Autocomplete */
'ace-autocomplete-primary-fg': '#444',
'ace-autocomplete-secondary-fg': '#8F8F8F',
'ace-autocomplete-highlighted-fg': '#000',
'ace-autocomplete-bg': '#FBFBFB',
'ace-autocomplete-border': 'lightgray',
'ace-autocomplete-link': '#16B378',
'ace-autocomplete-link-highlighted': '#009058',
'ace-autocomplete-active-line-bg': '#CAD6FA',
'ace-autocomplete-line-border-hover': '#ABBFFE',
'ace-autocomplete-line-bg-hover': 'rgba(233,233,253,0.4)',
/* Color Select */
'color-select-fg': '#262633',
'color-select-bg': 'white',
'color-select-shadow': 'rgba(38,38,51,0.6)',
'color-select-font-options-border': '#D9D9D9',
'color-select-font-option-fg': '#262633',
'color-select-font-option-bg': '#FFFFFF',
'color-select-font-option-bg-hover': '#F7F7F7',
'color-select-font-option-fg-selected': '#FFFFFF',
'color-select-font-option-bg-selected': '#262633',
'color-select-color-square-border': '#D9D9D9',
'color-select-color-square-border-empty': '#262633',
'color-select-input-fg': '#929299',
'color-select-input-bg': 'white',
'color-select-input-border': '#D9D9D9',
/* Highlighted Code */
'highlighted-code-block-bg': '#FFFFFF',
'highlighted-code-block-bg-disabled': '#E8E8E8',
'highlighted-code-fg': '#929299',
'highlighted-code-border': '#D9D9D9',
'highlighted-code-bg-disabled': '#E8E8E8',
};

@ -472,7 +472,7 @@ export class ApiServer {
// GET /api/session/access/active
// Returns active user and active org (if any)
this._app.get('/api/session/access/active', expressWrap(async (req, res) => {
const fullUser = await this._getFullUser(req);
const fullUser = await this._getFullUser(req, {includePrefs: true});
const domain = getOrgFromRequest(req);
const org = domain ? (await this._withSupportUserAllowedToView(
domain, req, (scope) => this._dbManager.getOrg(scope, domain)
@ -534,10 +534,10 @@ export class ApiServer {
}));
}
private async _getFullUser(req: Request): Promise<FullUser> {
private async _getFullUser(req: Request, options: {includePrefs?: boolean} = {}): Promise<FullUser> {
const mreq = req as RequestWithLogin;
const userId = getUserId(mreq);
const user = await this._dbManager.getUser(userId);
const user = await this._dbManager.getUser(userId, options);
if (!user) { throw new ApiError("unable to find user", 400); }
const fullUser = this._dbManager.makeFullUser(user);

@ -7,6 +7,7 @@ import {BaseEntity, BeforeInsert, Column, Entity, JoinTable, ManyToMany, OneToMa
import {Group} from "./Group";
import {Login} from "./Login";
import {Organization} from "./Organization";
import {Pref} from './Pref';
@Entity({name: 'users'})
export class User extends BaseEntity {
@ -34,6 +35,9 @@ export class User extends BaseEntity {
@OneToMany(type => Login, login => login.user)
public logins: Login[];
@OneToMany(type => Pref, pref => pref.user)
public prefs: Pref[];
@ManyToMany(type => Group)
@JoinTable({
name: 'group_users',

@ -482,8 +482,14 @@ export class HomeDBManager extends EventEmitter {
return await User.findOne({where: {ref}, relations: ["logins"]}) || undefined;
}
public async getUser(userId: number): Promise<User|undefined> {
return await User.findOne({where: {id: userId}, relations: ["logins"]}) || undefined;
public async getUser(
userId: number,
options: {includePrefs?: boolean} = {}
): Promise<User|undefined> {
const {includePrefs} = options;
const relations = ["logins"];
if (includePrefs) { relations.push("prefs"); }
return await User.findOne({where: {id: userId}, relations}) || undefined;
}
public async getFullUser(userId: number): Promise<FullUser> {
@ -505,7 +511,8 @@ export class HomeDBManager extends EventEmitter {
name: user.name,
picture: user.picture,
ref: user.ref,
locale: user.options?.locale
locale: user.options?.locale,
prefs: user.prefs?.find((p)=> p.orgId === null)?.prefs,
};
if (this.getAnonymousUserId() === user.id) {
result.anonymous = true;

Loading…
Cancel
Save