(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, ` export const cssSmallLinkButton = styled(basicButtonLink, `
display: flex; display: inline-flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
min-height: 26px; min-height: 26px;

View File

@ -10,12 +10,10 @@ import {leftPanelBasic} from 'app/client/ui/LeftPanelCommon';
import {pagePanels} from 'app/client/ui/PagePanels'; import {pagePanels} from 'app/client/ui/PagePanels';
import {SupportGristPage} from 'app/client/ui/SupportGristPage'; import {SupportGristPage} from 'app/client/ui/SupportGristPage';
import {createTopBarHome} from 'app/client/ui/TopBar'; import {createTopBarHome} from 'app/client/ui/TopBar';
import {transition} from 'app/client/ui/transitions';
import {cssBreadcrumbs, separator} from 'app/client/ui2018/breadcrumbs'; import {cssBreadcrumbs, separator} from 'app/client/ui2018/breadcrumbs';
import {basicButton} from 'app/client/ui2018/buttons'; import {basicButton} from 'app/client/ui2018/buttons';
import {toggle} from 'app/client/ui2018/checkbox'; import {toggle} from 'app/client/ui2018/checkbox';
import {mediaSmall, testId, theme, vars} from 'app/client/ui2018/cssVars'; 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 {cssLink, makeLinks} from 'app/client/ui2018/links';
import {SandboxingBootProbeDetails} from 'app/common/BootProbe'; import {SandboxingBootProbeDetails} from 'app/common/BootProbe';
import {commonUrls, getPageTitleSuffix} from 'app/common/gristUrls'; 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 {naturalCompare} from 'app/common/SortFunc';
import {getGristConfig} from 'app/common/urlUtils'; import {getGristConfig} from 'app/common/urlUtils';
import * as version from 'app/common/version'; import * as version from 'app/common/version';
import {Computed, Disposable, dom, DomContents, IDisposable, import {Computed, Disposable, dom, IDisposable,
IDisposableOwner, MultiHolder, Observable, styled} from 'grainjs'; IDisposableOwner, MultiHolder, Observable, styled} from 'grainjs';
import {AdminSection, AdminSectionItem, HidableToggle} from 'app/client/ui/AdminPanelCss';
const t = makeT('AdminPanel'); const t = makeT('AdminPanel');
@ -82,43 +81,41 @@ export class AdminPanel extends Disposable {
return cssPageContainer( return cssPageContainer(
dom.cls('clipboard'), dom.cls('clipboard'),
{tabIndex: "-1"}, {tabIndex: "-1"},
cssSection( dom.create(AdminSection, t('Support Grist'), [
cssSectionTitle(t('Support Grist')), dom.create(AdminSectionItem, {
this._buildItem(owner, {
id: 'telemetry', id: 'telemetry',
name: t('Telemetry'), name: t('Telemetry'),
description: t('Help us make Grist better'), description: t('Help us make Grist better'),
value: maybeSwitchToggle(this._supportGrist.getTelemetryOptInObservable()), value: dom.create(HidableToggle, this._supportGrist.getTelemetryOptInObservable()),
expandedContent: this._supportGrist.buildTelemetrySection(), expandedContent: this._supportGrist.buildTelemetrySection(),
}), }),
this._buildItem(owner, { dom.create(AdminSectionItem, {
id: 'sponsor', id: 'sponsor',
name: t('Sponsor'), name: t('Sponsor'),
description: t('Support Grist Labs on GitHub'), description: t('Support Grist Labs on GitHub'),
value: this._supportGrist.buildSponsorshipSmallButton(), value: this._supportGrist.buildSponsorshipSmallButton(),
expandedContent: this._supportGrist.buildSponsorshipSection(), expandedContent: this._supportGrist.buildSponsorshipSection(),
}), }),
), ]),
cssSection( dom.create(AdminSection, t('Security Settings'), [
cssSectionTitle(t('Security Settings')), dom.create(AdminSectionItem, {
this._buildItem(owner, {
id: 'sandboxing', id: 'sandboxing',
name: t('Sandboxing'), name: t('Sandboxing'),
description: t('Sandbox settings for data engine'), description: t('Sandbox settings for data engine'),
value: this._buildSandboxingDisplay(owner), value: this._buildSandboxingDisplay(owner),
expandedContent: this._buildSandboxingNotice(), expandedContent: this._buildSandboxingNotice(),
}), }),
), ]),
cssSection(
cssSectionTitle(t('Version')), dom.create(AdminSection, t('Version'), [
this._buildItem(owner, { dom.create(AdminSectionItem, {
id: 'version', id: 'version',
name: t('Current'), name: t('Current'),
description: t('Current version of Grist'), description: t('Current version of Grist'),
value: cssValueLabel(`Version ${version.version}`), value: cssValueLabel(`Version ${version.version}`),
}), }),
this._buildUpdates(owner), this._buildUpdates(owner),
), ]),
testId('admin-panel'), testId('admin-panel'),
); );
} }
@ -137,9 +134,9 @@ export class AdminPanel extends Disposable {
const configured = details.configured; const configured = details.configured;
return cssValueLabel( return cssValueLabel(
configured ? configured ?
(success ? cssHappy(t('OK') + `: ${flavor}`) : (success ? cssHappyText(t('OK') + `: ${flavor}`) :
cssError(t('Error') + `: ${flavor}`)) : cssErrorText(t('Error') + `: ${flavor}`)) :
cssError(t('unconfigured'))); 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) { private _buildUpdates(owner: MultiHolder) {
// We can be in those states: // We can be in those states:
enum State { 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', id: 'updates',
name: t('Updates'), name: t('Updates'),
description: dom('span', testId('admin-panel-updates-message'), dom.text(description)), 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', ` const cssPageContainer = styled('div', `
overflow: auto; overflow: auto;
padding: 40px; 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} { export const cssValueLabel = styled('div', `
& {
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', `
padding: 4px 8px; padding: 4px 8px;
color: ${theme.text}; color: ${theme.text};
border: 1px solid ${theme.inputBorder}; border: 1px solid ${theme.inputBorder};
border-radius: ${vars.controlBorderRadius}; border-radius: ${vars.controlBorderRadius};
&-empty {
visibility: hidden;
content: " ";
}
`); `);
// A wrapper for the version details panel. Shows two columns. // A wrapper for the version details panel. Shows two columns.
@ -591,10 +442,10 @@ const cssGrayed = styled('span', `
color: ${theme.lightText}; color: ${theme.lightText};
`); `);
export const cssError = styled('div', ` const cssErrorText = styled('span', `
color: ${theme.errorText}; color: ${theme.errorText};
`); `);
export const cssHappy = styled('div', ` const cssHappyText = styled('span', `
color: ${theme.controlFg}; 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, * This module export a component for editing some document settings consisting of the timezone,
* (new settings to be added here ...). * (new settings to be added here ...).
*/ */
import {cssSmallButton, cssSmallLinkButton} from 'app/client/components/Forms/styles';
import {GristDoc} from 'app/client/components/GristDoc'; import {GristDoc} from 'app/client/components/GristDoc';
import {ACIndexImpl} from 'app/client/lib/ACIndex'; import {ACIndexImpl} from 'app/client/lib/ACIndex';
import {ACSelectItem, buildACSelect} from 'app/client/lib/ACSelect'; 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 type {DocPageModel} from 'app/client/models/DocPageModel';
import {urlState} from 'app/client/models/gristUrlState'; import {urlState} from 'app/client/models/gristUrlState';
import {KoSaveableObservable} from 'app/client/models/modelUtil'; import {KoSaveableObservable} from 'app/client/models/modelUtil';
import {docListHeader} from 'app/client/ui/DocMenuCss'; import {AdminSection, AdminSectionItem} from 'app/client/ui/AdminPanelCss';
import {showTransientTooltip} from 'app/client/ui/tooltips'; import {hoverTooltip, showTransientTooltip} from 'app/client/ui/tooltips';
import {primaryButtonLink} from 'app/client/ui2018/buttons'; import {colors, mediaSmall, testId, theme} from 'app/client/ui2018/cssVars';
import {mediaSmall, testId, theme, vars} 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 {select} from 'app/client/ui2018/menus';
import {confirmModal} from 'app/client/ui2018/modals'; import {confirmModal} from 'app/client/ui2018/modals';
import {buildCurrencyPicker} from 'app/client/widgets/CurrencyPicker'; import {buildCurrencyPicker} from 'app/client/widgets/CurrencyPicker';
import {buildTZAutocomplete} from 'app/client/widgets/TZAutocomplete'; import {buildTZAutocomplete} from 'app/client/widgets/TZAutocomplete';
import {EngineCode} from 'app/common/DocumentSettings'; 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 {propertyCompare} from 'app/common/gutil';
import {getCurrency, locales} from 'app/common/Locales'; import {getCurrency, locales} from 'app/common/Locales';
import {Computed, Disposable, dom, fromKo, IDisposableOwner, styled} from 'grainjs'; import {Computed, Disposable, dom, fromKo, IDisposableOwner, styled} from 'grainjs';
@ -39,6 +41,11 @@ export class DocSettingsPage extends Disposable {
)) ))
.onWrite(val => this._setEngine(val)); .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) { constructor(private _gristDoc: GristDoc) {
super(); super();
} }
@ -48,50 +55,118 @@ export class DocSettingsPage extends Disposable {
const docPageModel = this._gristDoc.docPageModel; const docPageModel = this._gristDoc.docPageModel;
return cssContainer( return cssContainer(
cssHeader(t('Document Settings')), dom.create(AdminSection, t('Document Settings'), [
cssDataRow(t("Time Zone:")), dom.create(AdminSectionItem, {
cssDataRow( id: 'timezone',
dom.create(buildTZAutocomplete, moment, fromKo(this._timezone), (val) => this._timezone.saveOnly(val)) name: t('Time Zone'),
), description: t('Default for DateTime columns'),
cssDataRow(t("Locale:")), value: dom.create(cssTZAutoComplete, moment, fromKo(this._timezone), (val) => this._timezone.saveOnly(val)),
cssDataRow(dom.create(buildLocaleSelect, this._locale)), }),
cssDataRow(t("Currency:")), dom.create(AdminSectionItem, {
cssDataRow(dom.domComputed(fromKo(this._locale), (l) => id: 'locale',
dom.create(buildCurrencyPicker, fromKo(this._currency), (val) => this._currency.saveOnly(val), 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)})}) {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.create(AdminSection, t('Data Engine'), [
dom('span', '☠', // dom.create(AdminSectionItem, {
dom.style('cursor', 'pointer'), // id: 'timings',
dom.on('click', async () => { // 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(); await docPageModel.appModel.api.getDocAPI(docPageModel.currentDocId.get()!).forceReload();
document.location.reload(); 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()!);
}), }),
)),
cssDataRow(primaryButtonLink(t('API Console'), { 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', target: '_blank',
href: getApiConsoleLink(docPageModel), href: getApiConsoleLink(docPageModel),
})), }),
cssHeader(t('Webhooks'), cssBeta('Beta')), }),
cssDataRow(primaryButtonLink(t('Manage Webhooks'), urlState().setLinkUrl({docPage: 'webhook'}))),
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', ` const cssContainer = styled('div', `
overflow-y: auto; overflow-y: auto;
position: relative; position: relative;
@ -170,33 +238,129 @@ const cssContainer = styled('div', `
} }
`); `);
const cssHoverWrapper = styled('div', ` const cssCopyButton = styled('div', `
display: inline-block; position: absolute;
cursor: default; display: flex;
color: ${theme.lightText}; align-items: center;
transition: background 0.05s; justify-content: center;
height: 100%;
width: 24px;
right: 0;
top: 0;
--icon-color: ${theme.lightText};
&:hover { &:hover {
background: ${theme.lightHover}; --icon-color: ${colors.lightGreen};
} }
`); `);
// This matches the style used in showProfileModal in app/client/ui/AccountWidget. const cssIcon = styled(icon, `
const cssDataRow = styled('div', `
margin: 16px 0px;
font-size: ${vars.largeFontSize};
color: ${theme.text};
width: 360px;
`); `);
const cssBeta = styled('sup', ` const cssInput = styled('div', `
text-transform: uppercase; border: none;
color: ${theme.text}; outline: none;
font-size: ${vars.smallFontSize}; background: transparent;
margin-left: 8px; 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. // Check which engines can be selected in the UI, if any.
export function getSupportedEngineChoices(): EngineCode[] { export function getSupportedEngineChoices(): EngineCode[] {
const gristConfig: GristLoadConfig = (window as any).gristConfig || {}; const gristConfig: GristLoadConfig = (window as any).gristConfig || {};
return gristConfig.supportEngines || []; 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", helpLinkKeys: "https://support.getgrist.com/examples/2021-04-link-keys",
helpFilteringReferenceChoices: "https://support.getgrist.com/col-refs/#filtering-reference-choices-in-dropdown", helpFilteringReferenceChoices: "https://support.getgrist.com/col-refs/#filtering-reference-choices-in-dropdown",
helpSandboxing: "https://support.getgrist.com/self-managed/#how-do-i-sandbox-documents", helpSandboxing: "https://support.getgrist.com/self-managed/#how-do-i-sandbox-documents",
helpAPI: 'https://support.getgrist.com/api',
freeCoachingCall: getFreeCoachingCallUrl(), freeCoachingCall: getFreeCoachingCallUrl(),
contactSupport: getContactSupportUrl(), contactSupport: getContactSupportUrl(),
plans: "https://www.getgrist.com/pricing", plans: "https://www.getgrist.com/pricing",

View File

@ -109,7 +109,7 @@ describe('WebhookOverflow', function () {
async function openWebhookPageWithoutWaitForServer() { async function openWebhookPageWithoutWaitForServer() {
await openDocumentSettings(); 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 gu.scrollIntoView(button).click();
await waitForWebhookPage(); await waitForWebhookPage();
} }

View File

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