mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(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:
parent
3fc221f3e2
commit
00c8343e8a
@ -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;
|
||||||
|
@ -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};
|
||||||
`);
|
`);
|
||||||
|
194
app/client/ui/AdminPanelCss.ts
Normal file
194
app/client/ui/AdminPanelCss.ts
Normal 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};
|
||||||
|
`);
|
@ -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;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
@ -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",
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user