mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Add dropdown conditions
Summary: Dropdown conditions let you specify a predicate formula that's used to filter choices and references in their respective autocomplete dropdown menus. Test Plan: Python and browser tests (WIP). Reviewers: jarek, paulfitz Reviewed By: jarek Subscribers: dsagal, paulfitz Differential Revision: https://phab.getgrist.com/D4235
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import {detectCurrentLang, makeT} from 'app/client/lib/localization';
|
||||
import {checkName} from 'app/client/lib/nameUtils';
|
||||
import {AppModel, reportError} from 'app/client/models/AppModel';
|
||||
import {urlState} from 'app/client/models/gristUrlState';
|
||||
import * as css from 'app/client/ui/AccountPageCss';
|
||||
@@ -249,23 +250,6 @@ designed to ensure that you're the only person who can access your account, even
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We allow alphanumeric characters and certain common whitelisted characters (except at the start),
|
||||
* plus everything non-ASCII (for non-English alphabets, which we want to allow but it's hard to be
|
||||
* more precise about what exactly to allow).
|
||||
*/
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const VALID_NAME_REGEXP = /^(\w|[^\u0000-\u007F])(\w|[- ./'"()]|[^\u0000-\u007F])*$/;
|
||||
|
||||
/**
|
||||
* Test name against various rules to check if it is a valid username.
|
||||
*/
|
||||
export function checkName(name: string): boolean {
|
||||
return VALID_NAME_REGEXP.test(name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
const cssWarnings = styled(css.warning, `
|
||||
margin: -8px 0 0 110px;
|
||||
`);
|
||||
|
||||
@@ -14,6 +14,7 @@ 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';
|
||||
@@ -183,6 +184,7 @@ export class App extends DisposableWithEvents {
|
||||
|
||||
// Add the cssRootVars class to enable the variables in cssVars.
|
||||
attachCssRootVars(this.topAppModel.productFlavor);
|
||||
attachTheme();
|
||||
addViewportTag();
|
||||
this.autoDispose(createAppUI(this.topAppModel, this));
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {pagePanels} from 'app/client/ui/PagePanels';
|
||||
import {RightPanel} from 'app/client/ui/RightPanel';
|
||||
import {createTopBarDoc, createTopBarHome} from 'app/client/ui/TopBar';
|
||||
import {WelcomePage} from 'app/client/ui/WelcomePage';
|
||||
import {attachTheme, testId} from 'app/client/ui2018/cssVars';
|
||||
import {testId} from 'app/client/ui2018/cssVars';
|
||||
import {getPageTitleSuffix} from 'app/common/gristUrls';
|
||||
import {getGristConfig} from 'app/common/urlUtils';
|
||||
import {Computed, dom, IDisposable, IDisposableOwner, Observable, replaceContent, subscribe} from 'grainjs';
|
||||
@@ -27,9 +27,7 @@ import {Computed, dom, IDisposable, IDisposableOwner, Observable, replaceContent
|
||||
// TODO once #newui is gone, we don't need to worry about this being disposable.
|
||||
// appObj is the App object from app/client/ui/App.ts
|
||||
export function createAppUI(topAppModel: TopAppModel, appObj: App): IDisposable {
|
||||
const content = dom.maybeOwned(topAppModel.appObs, (owner, appModel) => {
|
||||
owner.autoDispose(attachTheme(appModel.currentTheme));
|
||||
|
||||
const content = dom.maybe(topAppModel.appObs, (appModel) => {
|
||||
return [
|
||||
createMainPage(appModel, appObj),
|
||||
buildSnackbarDom(appModel.notifier, appModel),
|
||||
|
||||
@@ -1,45 +1,112 @@
|
||||
import * as ace from 'ace-builds';
|
||||
import {Ace, loadAce} from 'app/client/lib/imports';
|
||||
import {theme, vars} from 'app/client/ui2018/cssVars';
|
||||
import {Theme} from 'app/common/ThemePrefs';
|
||||
import {getGristConfig} from 'app/common/urlUtils';
|
||||
import {BindableValue, Computed, dom, DomElementArg, Observable, styled, subscribeElem} from 'grainjs';
|
||||
import {gristThemeObs} from 'app/client/ui2018/theme';
|
||||
import {
|
||||
BindableValue,
|
||||
Disposable,
|
||||
DomElementArg,
|
||||
Observable,
|
||||
styled,
|
||||
subscribeElem,
|
||||
} from 'grainjs';
|
||||
|
||||
// ace-builds also has a minified build (src-min-noconflict), but we don't
|
||||
// use it since webpack already handles minification.
|
||||
require('ace-builds/src-noconflict/ext-static_highlight');
|
||||
require('ace-builds/src-noconflict/mode-python');
|
||||
require('ace-builds/src-noconflict/theme-chrome');
|
||||
require('ace-builds/src-noconflict/theme-dracula');
|
||||
|
||||
export interface ICodeOptions {
|
||||
gristTheme: Computed<Theme>;
|
||||
placeholder?: string;
|
||||
interface BuildCodeHighlighterOptions {
|
||||
maxLines?: number;
|
||||
}
|
||||
|
||||
let _ace: Ace;
|
||||
let _highlighter: any;
|
||||
let _PythonMode: any;
|
||||
let _aceDom: any;
|
||||
let _chrome: any;
|
||||
let _dracula: any;
|
||||
let _mode: any;
|
||||
|
||||
async function fetchAceModules() {
|
||||
return {
|
||||
ace: _ace || (_ace = await loadAce()),
|
||||
highlighter: _highlighter || (_highlighter = _ace.require('ace/ext/static_highlight')),
|
||||
PythonMode: _PythonMode || (_PythonMode = _ace.require('ace/mode/python').Mode),
|
||||
aceDom: _aceDom || (_aceDom = _ace.require('ace/lib/dom')),
|
||||
chrome: _chrome || (_chrome = _ace.require('ace/theme/chrome')),
|
||||
dracula: _dracula || (_dracula = _ace.require('ace/theme/dracula')),
|
||||
mode: _mode || (_mode = new _PythonMode()),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function that accepts a string of text representing code and returns
|
||||
* a highlighted version of it as an HTML string.
|
||||
*
|
||||
* This is useful for scenarios where highlighted code needs to be displayed outside of
|
||||
* grainjs. For example, when using `marked`'s `highlight` option to highlight code
|
||||
* blocks in a Markdown string.
|
||||
*/
|
||||
export async function buildCodeHighlighter(options: BuildCodeHighlighterOptions = {}) {
|
||||
const {maxLines} = options;
|
||||
const {highlighter, aceDom, chrome, dracula, mode} = await fetchAceModules();
|
||||
|
||||
return (code: string) => {
|
||||
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 = code.split(/\n/);
|
||||
if (lines.length > maxLines) {
|
||||
code = lines.slice(0, maxLines).join("\n") + " \u2026"; // Ellipsis
|
||||
}
|
||||
}
|
||||
|
||||
let aceThemeName: 'chrome' | 'dracula';
|
||||
let aceTheme: any;
|
||||
if (gristThemeObs().get().appearance === 'dark') {
|
||||
aceThemeName = 'dracula';
|
||||
aceTheme = dracula;
|
||||
} else {
|
||||
aceThemeName = 'chrome';
|
||||
aceTheme = chrome;
|
||||
}
|
||||
|
||||
// Rendering highlighted code gives you back the HTML to insert into the DOM, as well
|
||||
// as the CSS styles needed to apply the theme. The latter typically isn't included in
|
||||
// the document until an Ace editor is opened, so we explicitly import it here to avoid
|
||||
// leaving highlighted code blocks without a theme applied.
|
||||
const {html, css} = highlighter.render(code, mode, aceTheme, 1, true);
|
||||
aceDom.importCssString(css, `${aceThemeName}-highlighted-code`);
|
||||
return html;
|
||||
};
|
||||
}
|
||||
|
||||
interface BuildHighlightedCodeOptions extends BuildCodeHighlighterOptions {
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a block of highlighted `code`.
|
||||
*
|
||||
* Highlighting applies an appropriate Ace theme (Chrome or Dracula) based on
|
||||
* the current Grist theme, and automatically re-applies it whenever the Grist
|
||||
* theme changes.
|
||||
*/
|
||||
export function buildHighlightedCode(
|
||||
code: BindableValue<string>, options: ICodeOptions, ...args: DomElementArg[]
|
||||
owner: Disposable,
|
||||
code: BindableValue<string>,
|
||||
options: BuildHighlightedCodeOptions,
|
||||
...args: DomElementArg[]
|
||||
): HTMLElement {
|
||||
const {gristTheme, placeholder, maxLines} = options;
|
||||
const {enableCustomCss} = getGristConfig();
|
||||
const {placeholder, maxLines} = options;
|
||||
const codeText = Observable.create(owner, '');
|
||||
const codeTheme = Observable.create(owner, gristThemeObs().get());
|
||||
|
||||
const highlighter = ace.require('ace/ext/static_highlight');
|
||||
const PythonMode = ace.require('ace/mode/python').Mode;
|
||||
const aceDom = ace.require('ace/lib/dom');
|
||||
const chrome = ace.require('ace/theme/chrome');
|
||||
const dracula = ace.require('ace/theme/dracula');
|
||||
const mode = new PythonMode();
|
||||
|
||||
const codeText = Observable.create(null, '');
|
||||
const codeTheme = Observable.create(null, gristTheme.get());
|
||||
|
||||
function updateHighlightedCode(elem: HTMLElement) {
|
||||
async function updateHighlightedCode(elem: HTMLElement) {
|
||||
let text = codeText.get();
|
||||
if (!text) {
|
||||
elem.textContent = placeholder || '';
|
||||
return;
|
||||
}
|
||||
|
||||
const {highlighter, aceDom, chrome, dracula, mode} = await fetchAceModules();
|
||||
if (owner.isDisposed()) { return; }
|
||||
|
||||
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.)
|
||||
@@ -51,7 +118,7 @@ export function buildHighlightedCode(
|
||||
|
||||
let aceThemeName: 'chrome' | 'dracula';
|
||||
let aceTheme: any;
|
||||
if (codeTheme.get().appearance === 'dark' && !enableCustomCss) {
|
||||
if (codeTheme.get().appearance === 'dark') {
|
||||
aceThemeName = 'dracula';
|
||||
aceTheme = dracula;
|
||||
} else {
|
||||
@@ -69,15 +136,13 @@ export function buildHighlightedCode(
|
||||
}
|
||||
|
||||
return cssHighlightedCode(
|
||||
dom.autoDispose(codeText),
|
||||
dom.autoDispose(codeTheme),
|
||||
elem => subscribeElem(elem, code, (newCodeText) => {
|
||||
elem => subscribeElem(elem, code, async (newCodeText) => {
|
||||
codeText.set(newCodeText);
|
||||
updateHighlightedCode(elem);
|
||||
await updateHighlightedCode(elem);
|
||||
}),
|
||||
elem => subscribeElem(elem, gristTheme, (newCodeTheme) => {
|
||||
elem => subscribeElem(elem, gristThemeObs(), async (newCodeTheme) => {
|
||||
codeTheme.set(newCodeTheme);
|
||||
updateHighlightedCode(elem);
|
||||
await updateHighlightedCode(elem);
|
||||
}),
|
||||
...args,
|
||||
);
|
||||
@@ -95,9 +160,7 @@ export const cssCodeBlock = styled('div', `
|
||||
|
||||
const cssHighlightedCode = styled(cssCodeBlock, `
|
||||
position: relative;
|
||||
white-space: pre;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
border: 1px solid ${theme.highlightedCodeBorder};
|
||||
border-radius: 3px;
|
||||
min-height: 28px;
|
||||
@@ -110,20 +173,6 @@ const cssHighlightedCode = styled(cssCodeBlock, `
|
||||
& .ace_line {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
`);
|
||||
|
||||
export const cssFieldFormula = styled(buildHighlightedCode, `
|
||||
flex: auto;
|
||||
cursor: pointer;
|
||||
margin-top: 4px;
|
||||
padding-left: 24px;
|
||||
--icon-color: ${theme.accentIcon};
|
||||
|
||||
&-disabled-icon.formula_field_sidepane::before {
|
||||
--icon-color: ${theme.iconDisabled};
|
||||
}
|
||||
&-disabled {
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
`);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {createSessionObs} from 'app/client/lib/sessionObs';
|
||||
import {getTimeFromNow} from 'app/client/lib/timeUtils';
|
||||
import {DocPageModel} from 'app/client/models/DocPageModel';
|
||||
import {reportError} from 'app/client/models/errors';
|
||||
import {urlState} from 'app/client/models/gristUrlState';
|
||||
import {getTimeFromNow} from 'app/client/models/HomeModel';
|
||||
import {buildConfigContainer} from 'app/client/ui/RightPanel';
|
||||
import {buttonSelect} from 'app/client/ui2018/buttonSelect';
|
||||
import {testId, theme, vars} from 'app/client/ui2018/cssVars';
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
* Orgs, workspaces and docs are fetched asynchronously on build via the passed in API.
|
||||
*/
|
||||
import {loadUserManager} from 'app/client/lib/imports';
|
||||
import {getTimeFromNow} from 'app/client/lib/timeUtils';
|
||||
import {reportError} from 'app/client/models/AppModel';
|
||||
import {docUrl, urlState} from 'app/client/models/gristUrlState';
|
||||
import {getTimeFromNow, HomeModel, makeLocalViewSettings, ViewSettings} from 'app/client/models/HomeModel';
|
||||
import {HomeModel, makeLocalViewSettings, ViewSettings} from 'app/client/models/HomeModel';
|
||||
import {getWorkspaceInfo, workspaceName} from 'app/client/models/WorkspaceInfo';
|
||||
import {attachAddNewTip} from 'app/client/ui/AddNewTip';
|
||||
import * as css from 'app/client/ui/DocMenuCss';
|
||||
|
||||
@@ -2,7 +2,7 @@ import {makeT} from 'app/client/lib/localization';
|
||||
import {GristDoc} from 'app/client/components/GristDoc';
|
||||
import {BEHAVIOR, ColumnRec} from 'app/client/models/entities/ColumnRec';
|
||||
import {buildHighlightedCode, cssCodeBlock} from 'app/client/ui/CodeHighlight';
|
||||
import {cssBlockedCursor, cssLabel, cssRow} from 'app/client/ui/RightPanelStyles';
|
||||
import {cssBlockedCursor, cssFieldFormula, cssLabel, cssRow} from 'app/client/ui/RightPanelStyles';
|
||||
import {withInfoTooltip} from 'app/client/ui/tooltips';
|
||||
import {buildFormulaTriggers} from 'app/client/ui/TriggerFormulas';
|
||||
import {textButton} from 'app/client/ui2018/buttons';
|
||||
@@ -13,7 +13,6 @@ import {IconName} from 'app/client/ui2018/IconList';
|
||||
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 {CursorPos} from 'app/plugin/GristAPI';
|
||||
import {bundleChanges, Computed, dom, DomContents, DomElementArg, fromKo, MultiHolder,
|
||||
Observable, styled} from 'grainjs';
|
||||
@@ -139,6 +138,8 @@ export function buildFormulaConfig(
|
||||
// And close it dispose it when user opens up behavior menu.
|
||||
let formulaField: HTMLElement|null = null;
|
||||
|
||||
const focusFormulaField = () => setTimeout(() => formulaField?.focus(), 0);
|
||||
|
||||
// Helper function to clear temporary state (will be called when column changes or formula editor closes)
|
||||
const clearState = () => bundleChanges(() => {
|
||||
// For a detached editor, we may have already been disposed when user switched page.
|
||||
@@ -242,7 +243,7 @@ export function buildFormulaConfig(
|
||||
|
||||
// Converts data column to formula column.
|
||||
const convertDataColumnToFormulaOption = () => selectOption(
|
||||
() => (maybeFormula.set(true), formulaField?.focus()),
|
||||
() => (maybeFormula.set(true), focusFormulaField()),
|
||||
t("Clear and make into formula"), 'Script');
|
||||
|
||||
// Converts to empty column and opens up the editor. (label is the same, but this is used when we have no formula)
|
||||
@@ -270,15 +271,15 @@ export function buildFormulaConfig(
|
||||
const convertDataColumnToTriggerColumn = () => {
|
||||
maybeTrigger.set(true);
|
||||
// Open the formula editor.
|
||||
formulaField?.focus();
|
||||
focusFormulaField();
|
||||
};
|
||||
|
||||
// Converts formula column to trigger formula column.
|
||||
const convertFormulaToTrigger = () =>
|
||||
gristDoc.convertIsFormula([origColumn.id.peek()], {toFormula: false, noRecalc: false});
|
||||
|
||||
const setFormula = () => (maybeFormula.set(true), formulaField?.focus());
|
||||
const setTrigger = () => (maybeTrigger.set(true), formulaField?.focus());
|
||||
const setFormula = () => { maybeFormula.set(true); focusFormulaField(); };
|
||||
const setTrigger = () => { maybeTrigger.set(true); focusFormulaField(); };
|
||||
|
||||
// Actions on save formula. Those actions are using column that comes from FormulaEditor.
|
||||
// Formula editor scope is broader then RightPanel, it can be disposed after RightPanel is closed,
|
||||
@@ -325,16 +326,19 @@ export function buildFormulaConfig(
|
||||
const errorMessage = createFormulaErrorObs(owner, gristDoc, origColumn);
|
||||
// Helper that will create different flavors for formula builder.
|
||||
const formulaBuilder = (onSave: SaveHandler, canDetach?: boolean) => [
|
||||
cssRow(formulaField = buildFormula(
|
||||
origColumn,
|
||||
buildEditor,
|
||||
{
|
||||
gristTheme: gristDoc.currentTheme,
|
||||
disabled: disableOtherActions,
|
||||
canDetach,
|
||||
onSave,
|
||||
onCancel: clearState,
|
||||
})),
|
||||
cssRow(
|
||||
buildFormula(
|
||||
origColumn,
|
||||
buildEditor,
|
||||
{
|
||||
disabled: disableOtherActions,
|
||||
canDetach,
|
||||
onSave,
|
||||
onCancel: clearState,
|
||||
},
|
||||
(el) => { formulaField = el; },
|
||||
)
|
||||
),
|
||||
dom.maybe(errorMessage, errMsg => cssRow(cssError(errMsg), testId('field-error-count'))),
|
||||
];
|
||||
|
||||
@@ -419,7 +423,6 @@ export function buildFormulaConfig(
|
||||
}
|
||||
|
||||
interface BuildFormulaOptions {
|
||||
gristTheme: Computed<Theme>;
|
||||
disabled: Observable<boolean>;
|
||||
canDetach?: boolean;
|
||||
onSave?: SaveHandler;
|
||||
@@ -429,10 +432,12 @@ interface BuildFormulaOptions {
|
||||
function buildFormula(
|
||||
column: ColumnRec,
|
||||
buildEditor: BuildEditor,
|
||||
options: BuildFormulaOptions
|
||||
options: BuildFormulaOptions,
|
||||
...args: DomElementArg[]
|
||||
) {
|
||||
const {gristTheme, disabled, canDetach = true, onSave, onCancel} = options;
|
||||
return cssFieldFormula(column.formula, {gristTheme, maxLines: 2},
|
||||
const {disabled, canDetach = true, onSave, onCancel} = options;
|
||||
return dom.create(buildHighlightedCode, column.formula, {maxLines: 2},
|
||||
dom.cls(cssFieldFormula.className),
|
||||
dom.cls('formula_field_sidepane'),
|
||||
cssFieldFormula.cls('-disabled', disabled),
|
||||
cssFieldFormula.cls('-disabled-icon', use => !use(column.formula)),
|
||||
@@ -447,24 +452,10 @@ function buildFormula(
|
||||
onSave,
|
||||
onCancel,
|
||||
})),
|
||||
...args,
|
||||
);
|
||||
}
|
||||
|
||||
export const cssFieldFormula = styled(buildHighlightedCode, `
|
||||
flex: auto;
|
||||
cursor: pointer;
|
||||
margin-top: 4px;
|
||||
padding-left: 24px;
|
||||
--icon-color: ${theme.accentIcon};
|
||||
|
||||
&-disabled-icon.formula_field_sidepane::before {
|
||||
--icon-color: ${theme.lightText};
|
||||
}
|
||||
&-disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
`);
|
||||
|
||||
const cssToggleButton = styled(cssIconButton, `
|
||||
margin-left: 8px;
|
||||
background-color: ${theme.rightPanelToggleButtonDisabledBg};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as commands from 'app/client/components/commands';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {buildHighlightedCode} from 'app/client/ui/CodeHighlight';
|
||||
import {ShortcutKey, ShortcutKeyContent} from 'app/client/ui/ShortcutKey';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {cssLink} from 'app/client/ui2018/links';
|
||||
@@ -39,7 +40,9 @@ export type Tooltip =
|
||||
| 'uuid'
|
||||
| 'lookups'
|
||||
| 'formulaColumn'
|
||||
| 'accessRulesTableWide';
|
||||
| 'accessRulesTableWide'
|
||||
| 'setChoiceDropdownCondition'
|
||||
| 'setRefDropdownCondition';
|
||||
|
||||
export type TooltipContentFunc = (...domArgs: DomElementArg[]) => DomContents;
|
||||
|
||||
@@ -125,7 +128,26 @@ see or edit which parts of your document.')
|
||||
...args,
|
||||
),
|
||||
accessRulesTableWide: (...args: DomElementArg[]) => cssTooltipContent(
|
||||
dom('div', t('These rules are applied after all column rules have been processed, if applicable.'))
|
||||
dom('div', t('These rules are applied after all column rules have been processed, if applicable.')),
|
||||
...args,
|
||||
),
|
||||
setChoiceDropdownCondition: (...args: DomElementArg[]) => cssTooltipContent(
|
||||
dom('div',
|
||||
t('Filter displayed dropdown values with a condition.')
|
||||
),
|
||||
dom('div', {style: 'margin-top: 8px;'}, t('Example: {{example}}', {
|
||||
example: dom.create(buildHighlightedCode, 'choice not in $Categories', {}, {style: 'margin-top: 8px;'}),
|
||||
})),
|
||||
...args,
|
||||
),
|
||||
setRefDropdownCondition: (...args: DomElementArg[]) => cssTooltipContent(
|
||||
dom('div',
|
||||
t('Filter displayed dropdown values with a condition.')
|
||||
),
|
||||
dom('div', {style: 'margin-top: 8px;'}, t('Example: {{example}}', {
|
||||
example: dom.create(buildHighlightedCode, 'choice.Role == "Manager"', {}, {style: 'margin-top: 8px;'}),
|
||||
})),
|
||||
...args,
|
||||
),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {getTimeFromNow} from 'app/client/lib/timeUtils';
|
||||
import {docUrl, urlState} from 'app/client/models/gristUrlState';
|
||||
import {getTimeFromNow, HomeModel} from 'app/client/models/HomeModel';
|
||||
import {HomeModel} from 'app/client/models/HomeModel';
|
||||
import {makeDocOptionsMenu, makeRemovedDocOptionsMenu} from 'app/client/ui/DocMenu';
|
||||
import {transientInput} from 'app/client/ui/transientInput';
|
||||
import {colors, theme, vars} from 'app/client/ui2018/cssVars';
|
||||
|
||||
@@ -94,3 +94,18 @@ export const cssPinButton = styled('div', `
|
||||
export const cssNumericSpinner = styled(numericSpinner, `
|
||||
height: 28px;
|
||||
`);
|
||||
|
||||
export const cssFieldFormula = styled('div', `
|
||||
flex: auto;
|
||||
cursor: pointer;
|
||||
margin-top: 4px;
|
||||
padding-left: 24px;
|
||||
--icon-color: ${theme.accentIcon};
|
||||
|
||||
&-disabled-icon.formula_field_sidepane::before {
|
||||
--icon-color: ${theme.iconDisabled};
|
||||
}
|
||||
&-disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
`);
|
||||
|
||||
@@ -2,7 +2,7 @@ import {makeT} from 'app/client/lib/localization';
|
||||
import {AppModel} from 'app/client/models/AppModel';
|
||||
import * as css from 'app/client/ui/AccountPageCss';
|
||||
import {labeledSquareCheckbox} from 'app/client/ui2018/checkbox';
|
||||
import {prefersDarkModeObs} from 'app/client/ui2018/cssVars';
|
||||
import {prefersColorSchemeDarkObs} from 'app/client/ui2018/theme';
|
||||
import {select} from 'app/client/ui2018/menus';
|
||||
import {ThemeAppearance} from 'app/common/ThemePrefs';
|
||||
import {Computed, Disposable, dom, makeTestId, styled} from 'grainjs';
|
||||
@@ -20,10 +20,10 @@ export class ThemeConfig extends Disposable {
|
||||
private _appearance = Computed.create(this,
|
||||
this._themePrefs,
|
||||
this._syncWithOS,
|
||||
prefersDarkModeObs(),
|
||||
(_use, prefs, syncWithOS, prefersDarkMode) => {
|
||||
prefersColorSchemeDarkObs(),
|
||||
(_use, prefs, syncWithOS, prefersColorSchemeDark) => {
|
||||
if (syncWithOS) {
|
||||
return prefersDarkMode ? 'dark' : 'light';
|
||||
return prefersColorSchemeDark ? 'dark' : 'light';
|
||||
} else {
|
||||
return prefs.appearance;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import {AppModel, TopAppModelImpl, TopAppModelOptions} from 'app/client/models/A
|
||||
import {reportError, setUpErrorHandling} from 'app/client/models/errors';
|
||||
import {buildSnackbarDom} from 'app/client/ui/NotifyUI';
|
||||
import {addViewportTag} from 'app/client/ui/viewport';
|
||||
import {attachCssRootVars, attachTheme} from 'app/client/ui2018/cssVars';
|
||||
import {attachCssRootVars} from 'app/client/ui2018/cssVars';
|
||||
import {attachTheme} from 'app/client/ui2018/theme';
|
||||
import {BaseAPI} from 'app/common/BaseAPI';
|
||||
import {dom, DomContents} from 'grainjs';
|
||||
|
||||
@@ -16,22 +17,22 @@ const G = getBrowserGlobals('document', 'window');
|
||||
*/
|
||||
export function createAppPage(
|
||||
buildAppPage: (appModel: AppModel) => DomContents,
|
||||
modelOptions: TopAppModelOptions = {}) {
|
||||
modelOptions: TopAppModelOptions = {}
|
||||
) {
|
||||
setUpErrorHandling();
|
||||
|
||||
const topAppModel = TopAppModelImpl.create(null, {}, undefined, modelOptions);
|
||||
|
||||
addViewportTag();
|
||||
attachCssRootVars(topAppModel.productFlavor);
|
||||
attachTheme();
|
||||
setupLocale().catch(reportError);
|
||||
|
||||
// Add globals needed by test utils.
|
||||
G.window.gristApp = {
|
||||
testNumPendingApiRequests: () => BaseAPI.numPendingRequests(),
|
||||
};
|
||||
dom.update(document.body, dom.maybeOwned(topAppModel.appObs, (owner, appModel) => {
|
||||
owner.autoDispose(attachTheme(appModel.currentTheme));
|
||||
|
||||
dom.update(document.body, dom.maybe(topAppModel.appObs, (appModel) => {
|
||||
return [
|
||||
buildAppPage(appModel),
|
||||
buildSnackbarDom(appModel.notifier, appModel),
|
||||
|
||||
@@ -4,7 +4,8 @@ import {reportError, setErrorNotifier, setUpErrorHandling} from 'app/client/mode
|
||||
import {Notifier} from 'app/client/models/NotifyModel';
|
||||
import {buildSnackbarDom} from 'app/client/ui/NotifyUI';
|
||||
import {addViewportTag} from 'app/client/ui/viewport';
|
||||
import {attachCssRootVars, attachTheme, prefersColorSchemeThemeObs} from 'app/client/ui2018/cssVars';
|
||||
import {attachCssRootVars} from 'app/client/ui2018/cssVars';
|
||||
import {attachTheme} from 'app/client/ui2018/theme';
|
||||
import {BaseAPI} from 'app/common/BaseAPI';
|
||||
import {dom, DomContents} from 'grainjs';
|
||||
|
||||
@@ -21,6 +22,7 @@ export function createPage(buildPage: () => DomContents, options: {disableTheme?
|
||||
|
||||
addViewportTag();
|
||||
attachCssRootVars('grist');
|
||||
if (!disableTheme) { attachTheme(); }
|
||||
setupLocale().catch(reportError);
|
||||
|
||||
// Add globals needed by test utils.
|
||||
@@ -32,7 +34,6 @@ export function createPage(buildPage: () => DomContents, options: {disableTheme?
|
||||
setErrorNotifier(notifier);
|
||||
|
||||
dom.update(document.body, () => [
|
||||
disableTheme ? null : dom.autoDispose(attachTheme(prefersColorSchemeThemeObs())),
|
||||
buildPage(),
|
||||
buildSnackbarDom(notifier, null),
|
||||
]);
|
||||
|
||||
@@ -2,16 +2,17 @@
|
||||
// keyboard. Dropdown features a search input and reoders the list of
|
||||
// items to bring best matches at the top.
|
||||
|
||||
import { Disposable, dom, DomElementMethod, IOptionFull, makeTestId, Observable, styled } from "grainjs";
|
||||
import { theme, vars } from 'app/client/ui2018/cssVars';
|
||||
import { ACIndexImpl, ACIndexOptions, ACItem, buildHighlightedDom, HighlightFunc,
|
||||
normalizeText } from "app/client/lib/ACIndex";
|
||||
import { menuDivider } from "app/client/ui2018/menus";
|
||||
import { icon } from "app/client/ui2018/icons";
|
||||
import { cssMenuItem, defaultMenuOptions, IOpenController, IPopupOptions, setPopupToFunc } from "popweasel";
|
||||
import { mergeWith } from "lodash";
|
||||
import { getOptionFull, SimpleList } from "../lib/simpleList";
|
||||
import { makeT } from 'app/client/lib/localization';
|
||||
import { getOptionFull, SimpleList } from "app/client/lib/simpleList";
|
||||
import { theme, vars } from 'app/client/ui2018/cssVars';
|
||||
import { icon } from "app/client/ui2018/icons";
|
||||
import { menuDivider } from "app/client/ui2018/menus";
|
||||
import { Disposable, dom, DomElementMethod, IOptionFull, makeTestId, Observable, styled } from "grainjs";
|
||||
import mergeWith from "lodash/mergeWith";
|
||||
import { cssMenuItem, defaultMenuOptions, IOpenController, IPopupOptions, setPopupToFunc } from "popweasel";
|
||||
|
||||
|
||||
const t = makeT('searchDropdown');
|
||||
|
||||
|
||||
@@ -582,7 +582,7 @@ const cssInfoTooltipPopup = styled('div', `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: ${theme.popupBg};
|
||||
max-width: 200px;
|
||||
max-width: 240px;
|
||||
margin: 4px;
|
||||
padding: 0px;
|
||||
`);
|
||||
|
||||
Reference in New Issue
Block a user