(core) Updating UI for Document Settings

Summary: Updating UI for Document Settings, by reusing components from Admin panel

Test Plan: Existing

Reviewers: georgegevoian

Reviewed By: georgegevoian

Differential Revision: https://phab.getgrist.com/D4250
This commit is contained in:
Jarosław Sadziński 2024-05-10 14:54:37 +02:00
parent 3fc221f3e2
commit 00c8343e8a
7 changed files with 456 additions and 246 deletions

View File

@ -397,7 +397,7 @@ export const cssButtonGroup = styled('div', `
export const cssSmallLinkButton = styled(basicButtonLink, `
display: flex;
display: inline-flex;
align-items: center;
gap: 4px;
min-height: 26px;

View File

@ -10,12 +10,10 @@ import {leftPanelBasic} from 'app/client/ui/LeftPanelCommon';
import {pagePanels} from 'app/client/ui/PagePanels';
import {SupportGristPage} from 'app/client/ui/SupportGristPage';
import {createTopBarHome} from 'app/client/ui/TopBar';
import {transition} from 'app/client/ui/transitions';
import {cssBreadcrumbs, separator} from 'app/client/ui2018/breadcrumbs';
import {basicButton} from 'app/client/ui2018/buttons';
import {toggle} from 'app/client/ui2018/checkbox';
import {mediaSmall, testId, theme, vars} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {cssLink, makeLinks} from 'app/client/ui2018/links';
import {SandboxingBootProbeDetails} from 'app/common/BootProbe';
import {commonUrls, getPageTitleSuffix} from 'app/common/gristUrls';
@ -23,8 +21,9 @@ import {InstallAPI, InstallAPIImpl, LatestVersion} from 'app/common/InstallAPI';
import {naturalCompare} from 'app/common/SortFunc';
import {getGristConfig} from 'app/common/urlUtils';
import * as version from 'app/common/version';
import {Computed, Disposable, dom, DomContents, IDisposable,
import {Computed, Disposable, dom, IDisposable,
IDisposableOwner, MultiHolder, Observable, styled} from 'grainjs';
import {AdminSection, AdminSectionItem, HidableToggle} from 'app/client/ui/AdminPanelCss';
const t = makeT('AdminPanel');
@ -82,43 +81,41 @@ export class AdminPanel extends Disposable {
return cssPageContainer(
dom.cls('clipboard'),
{tabIndex: "-1"},
cssSection(
cssSectionTitle(t('Support Grist')),
this._buildItem(owner, {
dom.create(AdminSection, t('Support Grist'), [
dom.create(AdminSectionItem, {
id: 'telemetry',
name: t('Telemetry'),
description: t('Help us make Grist better'),
value: maybeSwitchToggle(this._supportGrist.getTelemetryOptInObservable()),
value: dom.create(HidableToggle, this._supportGrist.getTelemetryOptInObservable()),
expandedContent: this._supportGrist.buildTelemetrySection(),
}),
this._buildItem(owner, {
dom.create(AdminSectionItem, {
id: 'sponsor',
name: t('Sponsor'),
description: t('Support Grist Labs on GitHub'),
value: this._supportGrist.buildSponsorshipSmallButton(),
expandedContent: this._supportGrist.buildSponsorshipSection(),
}),
),
cssSection(
cssSectionTitle(t('Security Settings')),
this._buildItem(owner, {
]),
dom.create(AdminSection, t('Security Settings'), [
dom.create(AdminSectionItem, {
id: 'sandboxing',
name: t('Sandboxing'),
description: t('Sandbox settings for data engine'),
value: this._buildSandboxingDisplay(owner),
expandedContent: this._buildSandboxingNotice(),
}),
),
cssSection(
cssSectionTitle(t('Version')),
this._buildItem(owner, {
]),
dom.create(AdminSection, t('Version'), [
dom.create(AdminSectionItem, {
id: 'version',
name: t('Current'),
description: t('Current version of Grist'),
value: cssValueLabel(`Version ${version.version}`),
}),
this._buildUpdates(owner),
),
]),
testId('admin-panel'),
);
}
@ -137,9 +134,9 @@ export class AdminPanel extends Disposable {
const configured = details.configured;
return cssValueLabel(
configured ?
(success ? cssHappy(t('OK') + `: ${flavor}`) :
cssError(t('Error') + `: ${flavor}`)) :
cssError(t('unconfigured')));
(success ? cssHappyText(t('OK') + `: ${flavor}`) :
cssErrorText(t('Error') + `: ${flavor}`)) :
cssErrorText(t('unconfigured')));
}
);
}
@ -159,49 +156,6 @@ isolated from other documents and isolated from the network.'),
];
}
private _buildItem(owner: IDisposableOwner, options: {
id: string,
name: DomContents,
description: DomContents,
value: DomContents,
expandedContent?: DomContents,
}) {
const itemContent = [
cssItemName(options.name, testId(`admin-panel-item-name-${options.id}`)),
cssItemDescription(options.description),
cssItemValue(options.value,
testId(`admin-panel-item-value-${options.id}`),
dom.on('click', ev => ev.stopPropagation())),
];
if (options.expandedContent) {
const isCollapsed = Observable.create(owner, true);
return cssItem(
cssItemShort(
dom.domComputed(isCollapsed, (c) => cssCollapseIcon(c ? 'Expand' : 'Collapse')),
itemContent,
cssItemShort.cls('-expandable'),
dom.on('click', () => isCollapsed.set(!isCollapsed.get())),
),
cssExpandedContentWrap(
transition(isCollapsed, {
prepare(elem, close) { elem.style.maxHeight = close ? elem.scrollHeight + 'px' : '0'; },
run(elem, close) { elem.style.maxHeight = close ? '0' : elem.scrollHeight + 'px'; },
finish(elem, close) { elem.style.maxHeight = close ? '0' : 'unset'; },
}),
cssExpandedContent(
options.expandedContent,
),
),
testId(`admin-panel-item-${options.id}`),
);
} else {
return cssItem(
cssItemShort(itemContent),
testId(`admin-panel-item-${options.id}`),
);
}
}
private _buildUpdates(owner: MultiHolder) {
// We can be in those states:
enum State {
@ -365,7 +319,7 @@ isolated from other documents and isolated from the network.'),
}
});
return this._buildItem(owner, {
return dom.create(AdminSectionItem, {
id: 'updates',
name: t('Updates'),
description: dom('span', testId('admin-panel-updates-message'), dom.text(description)),
@ -422,10 +376,6 @@ isolated from other documents and isolated from the network.'),
}
}
function maybeSwitchToggle(value: Observable<boolean|null>): DomContents {
return toggle(value, dom.hide((use) => use(value) === null));
}
const cssPageContainer = styled('div', `
overflow: auto;
padding: 40px;
@ -440,111 +390,12 @@ const cssPageContainer = styled('div', `
}
`);
const cssSection = styled('div', `
padding: 24px;
max-width: 600px;
width: 100%;
margin: 16px auto;
border: 1px solid ${theme.widgetBorder};
border-radius: 4px;
@media ${mediaSmall} {
& {
width: auto;
padding: 12px;
margin: 8px;
}
}
`);
const cssSectionTitle = styled('div', `
height: 32px;
line-height: 32px;
margin-bottom: 16px;
font-size: ${vars.headerControlFontSize};
font-weight: ${vars.headerControlTextWeight};
`);
const cssItem = styled('div', `
margin-top: 8px;
`);
const cssItemShort = styled('div', `
display: flex;
flex-wrap: wrap;
align-items: center;
padding: 8px;
margin: 0 -8px;
border-radius: 4px;
&-expandable {
cursor: pointer;
}
&-expandable:hover {
background-color: ${theme.lightHover};
}
`);
const cssItemName = styled('div', `
width: 112px;
font-weight: bold;
font-size: ${vars.largeFontSize};
&:first-child {
margin-left: 24px;
}
@media ${mediaSmall} {
& {
width: calc(100% - 28px);
}
&:first-child {
margin-left: 0;
}
}
`);
const cssItemDescription = styled('div', `
margin-right: auto;
`);
const cssItemValue = styled('div', `
flex: none;
margin: -16px;
padding: 16px;
cursor: auto;
`);
const cssCollapseIcon = styled(icon, `
width: 24px;
height: 24px;
margin-right: 4px;
margin-left: -4px;
--icon-color: ${theme.lightText};
`);
const cssExpandedContentWrap = styled('div', `
transition: max-height 0.3s ease-in-out;
overflow: hidden;
max-height: 0;
`);
const cssExpandedContent = styled('div', `
margin-left: 24px;
padding: 24px 0;
border-bottom: 1px solid ${theme.widgetBorder};
.${cssItem.className}:last-child & {
padding-bottom: 0;
border-bottom: none;
}
`);
const cssValueLabel = styled('div', `
export const cssValueLabel = styled('div', `
padding: 4px 8px;
color: ${theme.text};
border: 1px solid ${theme.inputBorder};
border-radius: ${vars.controlBorderRadius};
&-empty {
visibility: hidden;
content: " ";
}
`);
// A wrapper for the version details panel. Shows two columns.
@ -591,10 +442,10 @@ const cssGrayed = styled('span', `
color: ${theme.lightText};
`);
export const cssError = styled('div', `
const cssErrorText = styled('span', `
color: ${theme.errorText};
`);
export const cssHappy = styled('div', `
const cssHappyText = styled('span', `
color: ${theme.controlFg};
`);

View File

@ -0,0 +1,194 @@
import {transition} from 'app/client/ui/transitions';
import {toggle} from 'app/client/ui2018/checkbox';
import {mediaSmall, testId, theme, vars} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {dom, DomContents, IDisposableOwner, Observable, styled} from 'grainjs';
export function HidableToggle(owner: IDisposableOwner, value: Observable<boolean|null>) {
return toggle(value, dom.hide((use) => use(value) === null));
}
export function AdminSection(owner: IDisposableOwner, title: DomContents, items: DomContents[]) {
return cssSection(
cssSectionTitle(title),
...items,
);
}
export function AdminSectionItem(owner: IDisposableOwner, options: {
id: string,
name?: DomContents,
description?: DomContents,
value?: DomContents,
expandedContent?: DomContents,
}) {
const itemContent = (...prefix: DomContents[]) => [
cssItemName(
...prefix,
options.name,
testId(`admin-panel-item-name-${options.id}`),
prefix.length ? cssItemName.cls('-prefixed') : null,
),
cssItemDescription(options.description),
cssItemValue(options.value,
testId(`admin-panel-item-value-${options.id}`),
dom.on('click', ev => ev.stopPropagation())),
];
if (options.expandedContent) {
const isCollapsed = Observable.create(owner, true);
return cssItem(
cssItemShort(
itemContent(dom.domComputed(isCollapsed, (c) => cssCollapseIcon(c ? 'Expand' : 'Collapse'))),
cssItemShort.cls('-expandable'),
dom.on('click', () => isCollapsed.set(!isCollapsed.get())),
),
cssExpandedContentWrap(
transition(isCollapsed, {
prepare(elem, close) { elem.style.maxHeight = close ? elem.scrollHeight + 'px' : '0'; },
run(elem, close) { elem.style.maxHeight = close ? '0' : elem.scrollHeight + 'px'; },
finish(elem, close) { elem.style.maxHeight = close ? '0' : 'unset'; },
}),
cssExpandedContent(
options.expandedContent,
),
),
testId(`admin-panel-item-${options.id}`),
);
} else {
return cssItem(
cssItemShort(itemContent()),
testId(`admin-panel-item-${options.id}`),
);
}
}
const cssSection = styled('div', `
padding: 24px;
max-width: 600px;
width: 100%;
margin: 16px auto;
border: 1px solid ${theme.widgetBorder};
border-radius: 4px;
& > div + div {
margin-top: 8px;
}
@media ${mediaSmall} {
& {
width: auto;
padding: 12px;
margin: 8px;
}
}
`);
const cssSectionTitle = styled('div', `
height: 32px;
line-height: 32px;
margin-bottom: 8px;
font-size: ${vars.headerControlFontSize};
font-weight: ${vars.headerControlTextWeight};
`);
const cssItem = styled('div', `
margin-top: 8px;
container-type: inline-size;
container-name: line;
`);
const cssItemShort = styled('div', `
display: flex;
flex-wrap: wrap;
align-items: center;
padding: 8px;
margin: 0 -8px;
border-radius: 4px;
&-expandable {
cursor: pointer;
}
&-expandable:hover {
background-color: ${theme.lightHover};
}
@container line (max-width: 500px) {
& {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
}
`);
const cssItemName = styled('div', `
width: 136px;
font-weight: bold;
display: flex;
align-items: center;
font-size: ${vars.largeFontSize};
padding-left: 24px;
&-prefixed {
padding-left: 0;
}
@container line (max-width: 500px) {
& {
padding-left: 0;
}
}
@media ${mediaSmall} {
& {
width: calc(100% - 28px);
padding-left: 0;
}
&:first-child {
margin-left: 0;
}
}
`);
const cssItemDescription = styled('div', `
margin-right: auto;
`);
const cssItemValue = styled('div', `
flex: none;
margin: -16px;
padding: 16px;
cursor: auto;
`);
const cssCollapseIcon = styled(icon, `
width: 24px;
height: 24px;
margin-right: 4px;
margin-left: -4px;
--icon-color: ${theme.lightText};
`);
const cssExpandedContentWrap = styled('div', `
transition: max-height 0.3s ease-in-out;
overflow: hidden;
max-height: 0;
`);
const cssExpandedContent = styled('div', `
margin-left: 24px;
padding: 24px 0;
border-bottom: 1px solid ${theme.widgetBorder};
.${cssItem.className}:last-child & {
padding-bottom: 0;
border-bottom: none;
}
@container line (max-width: 500px) {
& {
margin-left: 0px;
}
}
`);
export const cssValueLabel = styled('div', `
padding: 4px 8px;
color: ${theme.text};
border: 1px solid ${theme.inputBorder};
border-radius: ${vars.controlBorderRadius};
`);

View File

@ -2,6 +2,7 @@
* This module export a component for editing some document settings consisting of the timezone,
* (new settings to be added here ...).
*/
import {cssSmallButton, cssSmallLinkButton} from 'app/client/components/Forms/styles';
import {GristDoc} from 'app/client/components/GristDoc';
import {ACIndexImpl} from 'app/client/lib/ACIndex';
import {ACSelectItem, buildACSelect} from 'app/client/lib/ACSelect';
@ -11,16 +12,17 @@ import {reportError} from 'app/client/models/AppModel';
import type {DocPageModel} from 'app/client/models/DocPageModel';
import {urlState} from 'app/client/models/gristUrlState';
import {KoSaveableObservable} from 'app/client/models/modelUtil';
import {docListHeader} from 'app/client/ui/DocMenuCss';
import {showTransientTooltip} from 'app/client/ui/tooltips';
import {primaryButtonLink} from 'app/client/ui2018/buttons';
import {mediaSmall, testId, theme, vars} from 'app/client/ui2018/cssVars';
import {AdminSection, AdminSectionItem} from 'app/client/ui/AdminPanelCss';
import {hoverTooltip, showTransientTooltip} from 'app/client/ui/tooltips';
import {colors, mediaSmall, testId, theme} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {cssLink} from 'app/client/ui2018/links';
import {select} from 'app/client/ui2018/menus';
import {confirmModal} from 'app/client/ui2018/modals';
import {buildCurrencyPicker} from 'app/client/widgets/CurrencyPicker';
import {buildTZAutocomplete} from 'app/client/widgets/TZAutocomplete';
import {EngineCode} from 'app/common/DocumentSettings';
import {GristLoadConfig} from 'app/common/gristUrls';
import {commonUrls, GristLoadConfig} from 'app/common/gristUrls';
import {propertyCompare} from 'app/common/gutil';
import {getCurrency, locales} from 'app/common/Locales';
import {Computed, Disposable, dom, fromKo, IDisposableOwner, styled} from 'grainjs';
@ -39,6 +41,11 @@ export class DocSettingsPage extends Disposable {
))
.onWrite(val => this._setEngine(val));
private _engines = getSupportedEngineChoices().map((engine) => ({
value: engine,
label: engine === 'python3' ? t(`python3 (recommended)`) : t(`python2 (legacy)`),
}));
constructor(private _gristDoc: GristDoc) {
super();
}
@ -48,50 +55,118 @@ export class DocSettingsPage extends Disposable {
const docPageModel = this._gristDoc.docPageModel;
return cssContainer(
cssHeader(t('Document Settings')),
cssDataRow(t("Time Zone:")),
cssDataRow(
dom.create(buildTZAutocomplete, moment, fromKo(this._timezone), (val) => this._timezone.saveOnly(val))
),
cssDataRow(t("Locale:")),
cssDataRow(dom.create(buildLocaleSelect, this._locale)),
cssDataRow(t("Currency:")),
cssDataRow(dom.domComputed(fromKo(this._locale), (l) =>
dom.create(buildCurrencyPicker, fromKo(this._currency), (val) => this._currency.saveOnly(val),
{defaultCurrencyLabel: t("Local currency ({{currency}})", {currency: getCurrency(l)})})
)),
canChangeEngine ? cssDataRow([
// Small easter egg: you can click on the skull-and-crossbones to
// force a reload of the document.
cssDataRow(t("Engine (experimental {{span}} change at own risk):", {span:
dom('span', '☠',
dom.style('cursor', 'pointer'),
dom.on('click', async () => {
await docPageModel.appModel.api.getDocAPI(docPageModel.currentDocId.get()!).forceReload();
document.location.reload();
}))
})),
select(this._engine, getSupportedEngineChoices()),
]) : null,
cssHeader(t('API')),
cssDataRow(t("This document's ID (for API use):")),
cssDataRow(cssHoverWrapper(
dom('tt', docPageModel.currentDocId.get()),
dom.on('click', async (e, d) => {
e.stopImmediatePropagation();
e.preventDefault();
showTransientTooltip(d, t("Document ID copied to clipboard"), {
key: 'copy-document-id'
});
await copyToClipboard(docPageModel.currentDocId.get()!);
dom.create(AdminSection, t('Document Settings'), [
dom.create(AdminSectionItem, {
id: 'timezone',
name: t('Time Zone'),
description: t('Default for DateTime columns'),
value: dom.create(cssTZAutoComplete, moment, fromKo(this._timezone), (val) => this._timezone.saveOnly(val)),
}),
)),
cssDataRow(primaryButtonLink(t('API Console'), {
target: '_blank',
href: getApiConsoleLink(docPageModel),
})),
cssHeader(t('Webhooks'), cssBeta('Beta')),
cssDataRow(primaryButtonLink(t('Manage Webhooks'), urlState().setLinkUrl({docPage: 'webhook'}))),
dom.create(AdminSectionItem, {
id: 'locale',
name: t('Locale'),
description: t('For number and date formats'),
value: dom.create(cssLocalePicker, this._locale),
}),
dom.create(AdminSectionItem, {
id: 'currency',
name: t('Currency'),
description: t('For currency columns'),
value: dom.domComputed(fromKo(this._locale), (l) =>
dom.create(cssCurrencyPicker, fromKo(this._currency), (val) => this._currency.saveOnly(val),
{defaultCurrencyLabel: t("Local currency ({{currency}})", {currency: getCurrency(l)})})
)
}),
]),
dom.create(AdminSection, t('Data Engine'), [
// dom.create(AdminSectionItem, {
// id: 'timings',
// name: t('Formula times'),
// description: t('Find slow formulas'),
// value: dom('div', t('Coming soon')),
// expandedContent: dom('div', t(
// 'Once you start timing, Grist will measure the time it takes to evaluate each formula. ' +
// 'This allows diagnosing which formulas are responsible for slow performance when a ' +
// 'document is first open, or when a document responds to changes.'
// )),
// }),
dom.create(AdminSectionItem, {
id: 'reload',
name: t('Reload'),
description: t('Hard reset of data engine'),
value: cssSmallButton('Reload data engine', dom.on('click', async () => {
await docPageModel.appModel.api.getDocAPI(docPageModel.currentDocId.get()!).forceReload();
document.location.reload();
}))
}),
canChangeEngine ? dom.create(AdminSectionItem, {
id: 'python',
name: t('Python'),
description: t('Python version used'),
value: cssSelect(this._engine, this._engines),
}) : null,
]),
dom.create(AdminSection, t('API'), [
dom.create(AdminSectionItem, {
id: 'documentId',
name: t('Document ID'),
description: t('ID for API use'),
value: cssHoverWrapper(
cssInput(docPageModel.currentDocId.get(), {tabIndex: "-1"}, clickToSelect(), readonly()),
cssCopyButton(
cssIcon('Copy'),
hoverTooltip(t('Copy to clipboard'), {
key: TOOLTIP_KEY,
}),
copyHandler(() => docPageModel.currentDocId.get()!, t("Document ID copied to clipboard")),
),
),
expandedContent: dom('div',
cssWrap(
t('Document ID to use whenever the REST API calls for {{docId}}. See {{apiURL}}', {
apiURL: cssLink({href: commonUrls.helpAPI, target: '_blank'}, t('API documentation.')),
docId: dom('code', 'docId')
})
),
dom.domComputed(urlState().makeUrl({
api: true,
docPage: undefined,
doc: docPageModel.currentDocId.get(),
}), url => [
cssWrap(t('Base doc URL: {{docApiUrl}}', {
docApiUrl: cssCopyLink(
{href: url},
url,
copyHandler(() => url, t("API URL copied to clipboard")),
hoverTooltip(t('Copy to clipboard'), {
key: TOOLTIP_KEY,
}),
)
})),
]),
),
}),
dom.create(AdminSectionItem, {
id: 'api-console',
name: t('API Console'),
description: t('Try API calls from the browser'),
value: cssSmallLinkButton(t('API console'), {
target: '_blank',
href: getApiConsoleLink(docPageModel),
}),
}),
dom.create(AdminSectionItem, {
id: 'webhooks',
name: t('Webhooks'),
description: t('Notify other services on doc changes'),
value: cssSmallLinkButton(t('Manage webhooks'), urlState().setLinkUrl({docPage: 'webhook'})),
}),
]),
);
}
@ -151,13 +226,6 @@ function buildLocaleSelect(
);
}
const cssHeader = styled(docListHeader, `
margin-bottom: 0;
&:not(:first-of-type) {
margin-top: 40px;
}
`);
const cssContainer = styled('div', `
overflow-y: auto;
position: relative;
@ -170,33 +238,129 @@ const cssContainer = styled('div', `
}
`);
const cssHoverWrapper = styled('div', `
display: inline-block;
cursor: default;
color: ${theme.lightText};
transition: background 0.05s;
const cssCopyButton = styled('div', `
position: absolute;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 24px;
right: 0;
top: 0;
--icon-color: ${theme.lightText};
&:hover {
background: ${theme.lightHover};
--icon-color: ${colors.lightGreen};
}
`);
// This matches the style used in showProfileModal in app/client/ui/AccountWidget.
const cssDataRow = styled('div', `
margin: 16px 0px;
font-size: ${vars.largeFontSize};
color: ${theme.text};
width: 360px;
const cssIcon = styled(icon, `
`);
const cssBeta = styled('sup', `
text-transform: uppercase;
color: ${theme.text};
font-size: ${vars.smallFontSize};
margin-left: 8px;
const cssInput = styled('div', `
border: none;
outline: none;
background: transparent;
width: 100%;
min-width: 180px;
height: 100%;
padding: 5px;
padding-right: 20px;
overflow: hidden;
text-overflow: ellipsis;
`);
const cssHoverWrapper = styled('div', `
max-width: 280px;
text-overflow: ellipsis;
overflow: hidden;
text-wrap: nowrap;
display: inline-block;
cursor: pointer;
transition: all 0.05s;
border-radius: 4px;
border-color: ${theme.inputBorder};
border-style: solid;
border-width: 1px;
height: 30px;
align-items: center;
position: relative;
`);
// This matches the style used in showProfileModal in app/client/ui/AccountWidget.
// Check which engines can be selected in the UI, if any.
export function getSupportedEngineChoices(): EngineCode[] {
const gristConfig: GristLoadConfig = (window as any).gristConfig || {};
return gristConfig.supportEngines || [];
}
const cssSelect = styled(select, `
min-width: 170px; /* to match the width of the timezone picker */
`);
const TOOLTIP_KEY = 'copy-on-settings';
function copyHandler(value: () => string, confirmation: string) {
return dom.on('click', async (e, d) => {
e.stopImmediatePropagation();
e.preventDefault();
showTransientTooltip(d as Element, confirmation, {
key: TOOLTIP_KEY
});
await copyToClipboard(value());
});
}
function readonly() {
return [
{ contentEditable: 'false', spellcheck: 'false' },
];
}
function clickToSelect() {
return dom.on('click', (e) => {
e.preventDefault();
e.stopPropagation();
const range = document.createRange();
range.selectNodeContents(e.target as Node);
const selection = window.getSelection();
if (selection) {
selection.removeAllRanges();
selection.addRange(range);
}
});
}
// A version that is not underlined, and on hover mouse pointer indicates that copy is available
const cssCopyLink = styled(cssLink, `
word-wrap: break-word;
&:hover {
border-radius: 4px;
text-decoration: none;
background: ${theme.lightHover};
outline-color: ${theme.linkHover};
outline-offset: 1px;
}
`);
const cssAutoComplete = `
width: 172px;
cursor: pointer;
& input {
text-overflow: ellipsis;
padding-right: 24px;
}
`;
const cssTZAutoComplete = styled(buildTZAutocomplete, cssAutoComplete);
const cssCurrencyPicker = styled(buildCurrencyPicker, cssAutoComplete);
const cssLocalePicker = styled(buildLocaleSelect, cssAutoComplete);
const cssWrap = styled('p', `
overflow-wrap: anywhere;
& * {
word-break: break-all;
}
`);

View File

@ -88,6 +88,7 @@ export const commonUrls = {
helpLinkKeys: "https://support.getgrist.com/examples/2021-04-link-keys",
helpFilteringReferenceChoices: "https://support.getgrist.com/col-refs/#filtering-reference-choices-in-dropdown",
helpSandboxing: "https://support.getgrist.com/self-managed/#how-do-i-sandbox-documents",
helpAPI: 'https://support.getgrist.com/api',
freeCoachingCall: getFreeCoachingCallUrl(),
contactSupport: getContactSupportUrl(),
plans: "https://www.getgrist.com/pricing",

View File

@ -109,7 +109,7 @@ describe('WebhookOverflow', function () {
async function openWebhookPageWithoutWaitForServer() {
await openDocumentSettings();
const button = await driver.findContentWait('a', /Manage Webhooks/, 3000);
const button = await driver.findContentWait('a', /Manage Webhooks/i, 3000);
await gu.scrollIntoView(button).click();
await waitForWebhookPage();
}

View File

@ -281,9 +281,9 @@ async function getField(rowNum: number, col: string) {
}
async function openWebhookPage() {
await gu.wipeToasts();
await gu.openDocumentSettings();
const button = await driver.findContentWait('a', /Manage Webhooks/, 3000);
await gu.scrollIntoView(button).click();
await gu.scrollIntoView(driver.findContentWait('a', /Manage Webhooks/i, 3000)).click();
await waitForWebhookPage();
}