(core) Disable formula timing UI for non-owners

Summary:
For non-owners, the timing section of Document Settings is now disabled.
For non-editors, the "Reload" section is disabled.

Test Plan: Added a test case for timing being disabled.

Reviewers: jarek

Reviewed By: jarek

Differential Revision: https://phab.getgrist.com/D4275
This commit is contained in:
Dmitry S 2024-06-18 09:56:11 -04:00
parent 76d94483ad
commit 51a34835c5
3 changed files with 46 additions and 2 deletions

View File

@ -1,3 +1,4 @@
import {hoverTooltip} from 'app/client/ui/tooltips';
import {transition} from 'app/client/ui/transitions'; import {transition} from 'app/client/ui/transitions';
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';
@ -21,6 +22,7 @@ export function AdminSectionItem(owner: IDisposableOwner, options: {
description?: DomContents, description?: DomContents,
value?: DomContents, value?: DomContents,
expandedContent?: DomContents, expandedContent?: DomContents,
disabled?: false|string,
}) { }) {
const itemContent = (...prefix: DomContents[]) => [ const itemContent = (...prefix: DomContents[]) => [
cssItemName( cssItemName(
@ -34,7 +36,7 @@ export function AdminSectionItem(owner: IDisposableOwner, options: {
testId(`admin-panel-item-value-${options.id}`), testId(`admin-panel-item-value-${options.id}`),
dom.on('click', ev => ev.stopPropagation())), dom.on('click', ev => ev.stopPropagation())),
]; ];
if (options.expandedContent) { if (options.expandedContent && !options.disabled) {
const isCollapsed = Observable.create(owner, true); const isCollapsed = Observable.create(owner, true);
return cssItem( return cssItem(
cssItemShort( cssItemShort(
@ -56,7 +58,13 @@ export function AdminSectionItem(owner: IDisposableOwner, options: {
); );
} else { } else {
return cssItem( return cssItem(
cssItemShort(itemContent()), cssItemShort(itemContent(),
cssItemShort.cls('-disabled', Boolean(options.disabled)),
options.disabled ? hoverTooltip(options.disabled, {
placement: 'bottom-end',
modifiers: {offset: {offset: '0, -10'}},
}) : null,
),
testId(`admin-panel-item-${options.id}`), testId(`admin-panel-item-${options.id}`),
); );
} }
@ -109,6 +117,9 @@ const cssItemShort = styled('div', `
&-expandable:hover { &-expandable:hover {
background-color: ${theme.lightHover}; background-color: ${theme.lightHover};
} }
&-disabled {
opacity: .5;
}
@container line (max-width: 500px) { @container line (max-width: 500px) {
& { & {
@ -157,6 +168,10 @@ const cssItemValue = styled('div', `
margin: -16px; margin: -16px;
padding: 16px; padding: 16px;
cursor: auto; cursor: auto;
.${cssItemShort.className}-disabled & {
pointer-events: none;
}
`); `);
const cssCollapseIcon = styled(icon, ` const cssCollapseIcon = styled(icon, `

View File

@ -28,6 +28,7 @@ import {EngineCode} from 'app/common/DocumentSettings';
import {commonUrls, GristLoadConfig} from 'app/common/gristUrls'; import {commonUrls, GristLoadConfig} from 'app/common/gristUrls';
import {not, propertyCompare} from 'app/common/gutil'; import {not, propertyCompare} from 'app/common/gutil';
import {getCurrency, locales} from 'app/common/Locales'; import {getCurrency, locales} from 'app/common/Locales';
import {isOwner, isOwnerOrEditor} from 'app/common/roles';
import {Computed, Disposable, dom, fromKo, IDisposableOwner, makeTestId, Observable, styled} from 'grainjs'; import {Computed, Disposable, dom, fromKo, IDisposableOwner, makeTestId, Observable, styled} from 'grainjs';
import * as moment from 'moment-timezone'; import * as moment from 'moment-timezone';
@ -58,6 +59,8 @@ export class DocSettingsPage extends Disposable {
const canChangeEngine = getSupportedEngineChoices().length > 0; const canChangeEngine = getSupportedEngineChoices().length > 0;
const docPageModel = this._gristDoc.docPageModel; const docPageModel = this._gristDoc.docPageModel;
const isTimingOn = this._gristDoc.isTimingOn; const isTimingOn = this._gristDoc.isTimingOn;
const isDocOwner = isOwner(docPageModel.currentDoc.get());
const isDocEditor = isOwnerOrEditor(docPageModel.currentDoc.get());
return cssContainer( return cssContainer(
dom.create(AdminSection, t('Document Settings'), [ dom.create(AdminSection, t('Document Settings'), [
@ -115,6 +118,7 @@ export class DocSettingsPage extends Disposable {
'This allows diagnosing which formulas are responsible for slow performance when a ' + 'This allows diagnosing which formulas are responsible for slow performance when a ' +
'document is first opened, or when a document responds to changes.' 'document is first opened, or when a document responds to changes.'
)), )),
disabled: isDocOwner ? false : t('Only available to document owners'),
}), }),
dom.create(AdminSectionItem, { dom.create(AdminSectionItem, {
@ -122,6 +126,7 @@ export class DocSettingsPage extends Disposable {
name: t('Reload'), name: t('Reload'),
description: t('Hard reset of data engine'), description: t('Hard reset of data engine'),
value: cssSmallButton(t('Reload data engine'), dom.on('click', this._reloadEngine.bind(this, true))), value: cssSmallButton(t('Reload data engine'), dom.on('click', this._reloadEngine.bind(this, true))),
disabled: isDocEditor ? false : t('Only available to document editors'),
}), }),
canChangeEngine ? dom.create(AdminSectionItem, { canChangeEngine ? dom.create(AdminSectionItem, {

View File

@ -166,6 +166,30 @@ describe("Timing", function () {
await driver.findWait('.test-raw-data-list', 2000); await driver.findWait('.test-raw-data-list', 2000);
assert.deepEqual(await driver.findAll('.test-raw-data-table-id', e => e.getText()), ['Table1']); assert.deepEqual(await driver.findAll('.test-raw-data-table-id', e => e.getText()), ['Table1']);
}); });
it('should be disabled for non-owners', async function() {
await userApi.updateDocPermissions(docId, {users: {
[gu.translateUser('user2').email]: 'editors',
}});
const session = await gu.session().teamSite.user('user2').login();
await session.loadDoc(`/doc/${docId}`);
await gu.openDocumentSettings();
const start = driver.find('.test-settings-timing-start');
assert.equal(await start.isPresent(), true);
// Check that we have an informative tooltip.
await start.mouseMove();
assert.match(await driver.findWait('.test-tooltip', 2000).getText(), /Only available to document owners/);
// Nothing should happen on click. We click the location rather than the element, since the
// element isn't actually clickable.
await start.mouseMove();
await driver.withActions(a => a.press().release());
await driver.sleep(100);
assert.equal(await driver.find(".test-settings-timing-modal").isPresent(), false);
});
}); });
const element = (testId: string) => ({ const element = (testId: string) => ({